mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-19 22:58:15 +00:00
Merge branch 'feat/datasource-engine' into 'release/1.0.0'
Feat/datasource engine See merge request !1022163
This commit is contained in:
commit
af7cf73939
3
packages/datasource-engine/.eslintignore
Normal file
3
packages/datasource-engine/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/node_modules
|
||||||
|
/es
|
||||||
|
/lib
|
||||||
@ -1,6 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: '../../.eslintrc.js',
|
extends: '../../.eslintrc',
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-parameter-properties': 1,
|
'@typescript-eslint/no-parameter-properties': 0,
|
||||||
}
|
'no-param-reassign': 0,
|
||||||
}
|
'max-len': 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
1
packages/datasource-engine/.gitignore
vendored
1
packages/datasource-engine/.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/es/
|
/es/
|
||||||
/lib/
|
/lib/
|
||||||
|
/dist
|
||||||
|
|||||||
5
packages/datasource-engine/.prettierrc.js
Normal file
5
packages/datasource-engine/.prettierrc.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
printWidth: 120,
|
||||||
|
};
|
||||||
8
packages/datasource-engine/ava.config.js
Normal file
8
packages/datasource-engine/ava.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
babel: {
|
||||||
|
compileEnhancements: false,
|
||||||
|
},
|
||||||
|
files: ['./test/core/*.ts', './test/scenes/**/*.test.ts'],
|
||||||
|
require: ['ts-node/register/transpile-only'],
|
||||||
|
extensions: ['ts'],
|
||||||
|
};
|
||||||
@ -1 +0,0 @@
|
|||||||
export type * from '../../es/handlers/fetch';
|
|
||||||
@ -1 +0,0 @@
|
|||||||
module.exports = require('../../lib/handlers/fetch').default;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export type * from '../../es/handlers/mtop';
|
|
||||||
@ -1 +0,0 @@
|
|||||||
module.exports = require('../../lib/handlers/mtop').default;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export type * from '../../es/handlers/url-params';
|
|
||||||
@ -1 +0,0 @@
|
|||||||
module.exports = require('../../lib/handlers/url-params').default;
|
|
||||||
1
packages/datasource-engine/interpret.d.ts
vendored
Normal file
1
packages/datasource-engine/interpret.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { create } from './dist/interpret';
|
||||||
1
packages/datasource-engine/interpret.js
Normal file
1
packages/datasource-engine/interpret.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/interpret');
|
||||||
@ -1,34 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "@ali/lowcode-datasource-engine",
|
"name": "@ali/lowcode-datasource-engine",
|
||||||
"version": "0.1.16",
|
"version": "1.0.2-alpha.3",
|
||||||
"description": "DataSource Engine for lowcode",
|
"main": "dist/index.js",
|
||||||
"main": "lib/index.js",
|
|
||||||
"module": "es/index.js",
|
|
||||||
"typings": "es/index.d.ts",
|
|
||||||
"files": [
|
"files": [
|
||||||
"handlers",
|
"dist",
|
||||||
"src",
|
"src",
|
||||||
"lib",
|
"interpret*",
|
||||||
"es"
|
"runtime*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsc --watch",
|
"build": "rm -rf dist && tsc --outDir ./dist --module esnext",
|
||||||
"clean": "rm -rf es lib",
|
"test": "ava",
|
||||||
"build": "rm -rf es lib && tsc --module esnext --target es6 && mv lib es && tsc",
|
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"author": "",
|
"typings": "dist/index.d.ts",
|
||||||
"license": "ISC",
|
"dependencies": {
|
||||||
"publishConfig": {
|
"@ali/lowcode-types": "1.0.13-alpha.2",
|
||||||
"registry": "https://registry.npm.alibaba-inc.com"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^3.9.7"
|
"typescript": "^3.9.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@ali/universal-mtop": "^5.1.9",
|
"@ava/babel": "^1.0.1",
|
||||||
"query-string": "^6.13.1",
|
"@types/sinon": "^9.0.5",
|
||||||
"tslib": "^2.0.1",
|
"ava": "3.11.1",
|
||||||
"universal-request": "^2.2.0"
|
"get-port": "^5.1.1",
|
||||||
|
"json5": "^2.1.3",
|
||||||
|
"sinon": "^9.0.3",
|
||||||
|
"ts-node": "^8.10.2",
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npm.alibaba-inc.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
packages/datasource-engine/readme.md
Normal file
51
packages/datasource-engine/readme.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
## 关于 @ali/lc-datasource-engine
|
||||||
|
|
||||||
|
低代码引擎数据源核心代码
|
||||||
|
|
||||||
|
## doc
|
||||||
|
|
||||||
|
[原理介绍](https://yuque.antfin-inc.com/docs/share/6ba9dab7-0712-4302-a5bb-b17d4a5f8505?# 《DataSource Engine》)
|
||||||
|
|
||||||
|
|
||||||
|
[fetch流程图](https://yuque.antfin-inc.com/docs/share/e9baef9a-3586-40fc-8708-eaeee0d7937e?# 《fetch 流程》)
|
||||||
|
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 面向运行时渲染,直接给 schema
|
||||||
|
import { create } from '@ali/lowcode-datasource-engine/interpret';
|
||||||
|
|
||||||
|
// 面向出码,需要给处理过后的内容
|
||||||
|
import { create } from '@ali/lowcode-datasource-engine/runtime';
|
||||||
|
|
||||||
|
import { createFetchHandler } from '@ali/lowcode-datasource-fetch-handler';
|
||||||
|
|
||||||
|
import { createMtopHandler } from '@ali/lowcode-datasource-mtop-handler';
|
||||||
|
|
||||||
|
// dataSource 可以是 schema 协议内容 或者是运行时的转化后的配置内容 (出码专用)
|
||||||
|
|
||||||
|
|
||||||
|
// context 上下文(setState 为必选)
|
||||||
|
const dataSourceEngine = create(dataSource, context, {
|
||||||
|
requestHandlersMap: { // 可选参数,以下内容为当前默认的内容
|
||||||
|
urlParams: handlersMap.urlParams('?bar=1&test=2'),
|
||||||
|
fetch: createFetchHandler,
|
||||||
|
mtop: createMtopHandler
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.log(dsf.dataSourceMap) // 符合集团协议的 datasourceMap https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#QUSn5
|
||||||
|
|
||||||
|
dsf.dataSourceMap['id'].load() // 加载
|
||||||
|
|
||||||
|
dsf.dataSourceMap['id'].status // 获取状态
|
||||||
|
|
||||||
|
dsf.dataSourceMap['id'].data // 获取数据
|
||||||
|
|
||||||
|
dsf.dataSourceMap['id'].error // 获取错误信息
|
||||||
|
|
||||||
|
dsf.reloadDataSource(); // 刷新所有数据源
|
||||||
|
|
||||||
|
```
|
||||||
1
packages/datasource-engine/runtime.d.ts
vendored
Normal file
1
packages/datasource-engine/runtime.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { create } from './dist/runtime';
|
||||||
1
packages/datasource-engine/runtime.js
Normal file
1
packages/datasource-engine/runtime.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/runtime/index');
|
||||||
@ -1,124 +0,0 @@
|
|||||||
import {
|
|
||||||
DataSourceConfig,
|
|
||||||
DataSourceEngineOptions,
|
|
||||||
IDataSourceEngine,
|
|
||||||
IDataSourceEngineFactory,
|
|
||||||
IRuntimeContext,
|
|
||||||
} from '../types';
|
|
||||||
import { RuntimeDataSource } from './RuntimeDataSource';
|
|
||||||
|
|
||||||
export class DataSourceEngine implements IDataSourceEngine {
|
|
||||||
private _dataSourceMap: Record<string, RuntimeDataSource> = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private _dataSourceConfig: DataSourceConfig,
|
|
||||||
private _runtimeContext: IRuntimeContext,
|
|
||||||
private _options?: DataSourceEngineOptions,
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
|
||||||
_dataSourceConfig.list?.forEach((ds) => {
|
|
||||||
// 确保数据源都有处理器
|
|
||||||
const requestHandler = ds.requestHandler || _options?.requestHandlersMap?.[ds.type];
|
|
||||||
if (!requestHandler) {
|
|
||||||
throw new Error(`No request handler for "${ds.type}" data source`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._dataSourceMap[ds.id] = new RuntimeDataSource(
|
|
||||||
ds.id,
|
|
||||||
ds.type,
|
|
||||||
getValue(ds.options) || {},
|
|
||||||
requestHandler.bind(_runtimeContext),
|
|
||||||
ds.dataHandler ? ds.dataHandler.bind(_runtimeContext) : undefined,
|
|
||||||
(data) => {
|
|
||||||
_runtimeContext.setState({ [ds.id]: data });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public get dataSourceMap() {
|
|
||||||
return this._dataSourceMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async reloadDataSource() {
|
|
||||||
try {
|
|
||||||
const allDataSourceConfigList = this._dataSourceConfig.list || [];
|
|
||||||
|
|
||||||
// urlParams 类型的优先加载
|
|
||||||
for (const ds of allDataSourceConfigList) {
|
|
||||||
if (ds.type === 'urlParams' && (getValue(ds.isInit) ?? true)) {
|
|
||||||
await this._dataSourceMap[ds.id].load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 然后是所有其他的
|
|
||||||
const remainDataSourceConfigList = allDataSourceConfigList.filter((x) => x.type !== 'urlParams');
|
|
||||||
|
|
||||||
// 先发起异步的
|
|
||||||
const asyncLoadings: Array<Promise<unknown>> = [];
|
|
||||||
for (const ds of remainDataSourceConfigList) {
|
|
||||||
if (getValue(ds.isInit) ?? true) {
|
|
||||||
const options = getValue(ds.options);
|
|
||||||
if (options && !options.isSync) {
|
|
||||||
this._dataSourceMap[ds.id].setOptions(options);
|
|
||||||
asyncLoadings.push(this._dataSourceMap[ds.id].load(options?.params).catch(() => {}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 再按先后顺序发起同步请求
|
|
||||||
for (const ds of remainDataSourceConfigList) {
|
|
||||||
if (getValue(ds.isInit) ?? true) {
|
|
||||||
const options = getValue(ds.options);
|
|
||||||
if (options && options.isSync) {
|
|
||||||
this._dataSourceMap[ds.id].setOptions(options);
|
|
||||||
await this._dataSourceMap[ds.id].load(options?.params);
|
|
||||||
await sleep(0); // TODO: 如何优雅地解决 setState 的异步问题?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// ignore error
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(asyncLoadings);
|
|
||||||
} finally {
|
|
||||||
const allDataHandler = this._dataSourceConfig.dataHandler;
|
|
||||||
if (allDataHandler) {
|
|
||||||
await allDataHandler(this._getDataMapOfAll());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getDataMapOfAll(): Record<string, unknown> {
|
|
||||||
const dataMap: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
Object.entries(this._dataSourceMap).forEach(([dsId, ds]) => {
|
|
||||||
dataMap[dsId] = ds.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
return dataMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const create: IDataSourceEngineFactory['create'] = (dataSourceConfig, runtimeContext, options) => {
|
|
||||||
return new DataSourceEngine(dataSourceConfig, runtimeContext, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getValue<T>(valueOrValueGetter: T | (() => T)): T;
|
|
||||||
function getValue<T extends boolean>(valueOrValueGetter: T | (() => T)): T | undefined {
|
|
||||||
if (typeof valueOrValueGetter === 'function') {
|
|
||||||
try {
|
|
||||||
return valueOrValueGetter();
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return valueOrValueGetter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(ms = 0) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
import { DataSourceOptions, IRuntimeDataSource, RequestHandler, RuntimeDataSourceStatus } from '../types';
|
|
||||||
import { DataSourceResponse } from '../types/DataSourceResponse';
|
|
||||||
|
|
||||||
export class RuntimeDataSource<
|
|
||||||
TParams extends Record<string, unknown> = Record<string, unknown>,
|
|
||||||
TRequestResult = unknown,
|
|
||||||
TResultData = unknown
|
|
||||||
> implements IRuntimeDataSource<TParams, TResultData> {
|
|
||||||
private _status: RuntimeDataSourceStatus = RuntimeDataSourceStatus.Initial;
|
|
||||||
|
|
||||||
private _data?: TResultData;
|
|
||||||
|
|
||||||
private _error?: Error;
|
|
||||||
|
|
||||||
private _latestOptions: DataSourceOptions<TParams>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private _id: string,
|
|
||||||
private _type: string,
|
|
||||||
private _initialOptions: DataSourceOptions<TParams>,
|
|
||||||
private _requestHandler: RequestHandler<DataSourceOptions<TParams>, DataSourceResponse<TRequestResult>>,
|
|
||||||
private _dataHandler:
|
|
||||||
| ((
|
|
||||||
data: DataSourceResponse<TRequestResult> | undefined,
|
|
||||||
error: unknown | undefined,
|
|
||||||
) => TResultData | Promise<TResultData>)
|
|
||||||
| undefined,
|
|
||||||
private _onLoaded: (data: TResultData) => void,
|
|
||||||
) {
|
|
||||||
this._latestOptions = _initialOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get data() {
|
|
||||||
return this._data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get error() {
|
|
||||||
return this._error;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async load(params?: TParams): Promise<TResultData> {
|
|
||||||
try {
|
|
||||||
this._latestOptions = {
|
|
||||||
...this._latestOptions,
|
|
||||||
params: {
|
|
||||||
...this._latestOptions.params,
|
|
||||||
...params,
|
|
||||||
} as TParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._status = RuntimeDataSourceStatus.Loading;
|
|
||||||
|
|
||||||
const data = await this._request(this._latestOptions);
|
|
||||||
|
|
||||||
this._status = RuntimeDataSourceStatus.Loaded;
|
|
||||||
|
|
||||||
this._onLoaded(data);
|
|
||||||
|
|
||||||
this._data = data;
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
this._error = err;
|
|
||||||
this._status = RuntimeDataSourceStatus.Error;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setOptions(options: DataSourceOptions<TParams>) {
|
|
||||||
this._latestOptions = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _request(options: DataSourceOptions<TParams>) {
|
|
||||||
try {
|
|
||||||
const response = await this._requestHandler(options);
|
|
||||||
|
|
||||||
const data = this._dataHandler
|
|
||||||
? await this._dataHandler(response, undefined)
|
|
||||||
: ((response.data as unknown) as TResultData);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
if (this._dataHandler) {
|
|
||||||
const data = await this._dataHandler(undefined, err);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
143
packages/datasource-engine/src/core/RuntimeDataSourceItem.ts
Normal file
143
packages/datasource-engine/src/core/RuntimeDataSourceItem.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/indent */
|
||||||
|
import {
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
IRuntimeDataSource,
|
||||||
|
RequestHandler,
|
||||||
|
RuntimeDataSourceConfig,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
RuntimeOptionsConfig,
|
||||||
|
UrlParamsHandler,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
class RuntimeDataSourceItem<TParams extends Record<string, unknown> = Record<string, unknown>, TResultData = unknown>
|
||||||
|
implements IRuntimeDataSource<TParams, TResultData> {
|
||||||
|
private _data?: TResultData;
|
||||||
|
|
||||||
|
private _error?: Error;
|
||||||
|
|
||||||
|
private _status = RuntimeDataSourceStatus.Initial;
|
||||||
|
|
||||||
|
private _dataSourceConfig: RuntimeDataSourceConfig;
|
||||||
|
|
||||||
|
private _request: RequestHandler<{ data: TResultData }> | UrlParamsHandler<TResultData>;
|
||||||
|
|
||||||
|
private _context: IDataSourceRuntimeContext;
|
||||||
|
|
||||||
|
private _options?: RuntimeOptionsConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
dataSourceConfig: RuntimeDataSourceConfig,
|
||||||
|
request: RequestHandler<{ data: TResultData }> | UrlParamsHandler<TResultData>,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) {
|
||||||
|
this._dataSourceConfig = dataSourceConfig;
|
||||||
|
this._request = request;
|
||||||
|
this._context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get error() {
|
||||||
|
return this._error;
|
||||||
|
}
|
||||||
|
|
||||||
|
get status() {
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(params?: TParams) {
|
||||||
|
if (!this._dataSourceConfig) return;
|
||||||
|
// 考虑没有绑定对应的 handler 的情况
|
||||||
|
if (!this._request) {
|
||||||
|
this._error = new Error(`no ${this._dataSourceConfig.type} handler provide`);
|
||||||
|
this._status = RuntimeDataSourceStatus.Error;
|
||||||
|
throw this._error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: urlParams 有没有更好的处理方式
|
||||||
|
if (this._dataSourceConfig.type === 'urlParams') {
|
||||||
|
const response = await (this._request as UrlParamsHandler<TResultData>)(this._context);
|
||||||
|
this._context.setState({
|
||||||
|
[this._dataSourceConfig.id]: response,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._data = response;
|
||||||
|
this._status = RuntimeDataSourceStatus.Loaded;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._dataSourceConfig.options) {
|
||||||
|
throw new Error(`${this._dataSourceConfig.id} has no options`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this._dataSourceConfig.options === 'function') {
|
||||||
|
this._options = this._dataSourceConfig.options();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 考虑转换之后是 null 的场景
|
||||||
|
if (!this._options) {
|
||||||
|
throw new Error(`${this._dataSourceConfig.id} options transform error`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 临时变量存,每次可能结果不一致,不做缓存
|
||||||
|
let shouldFetch = true;
|
||||||
|
|
||||||
|
if (this._dataSourceConfig.shouldFetch) {
|
||||||
|
if (typeof this._dataSourceConfig.shouldFetch === 'function') {
|
||||||
|
shouldFetch = this._dataSourceConfig.shouldFetch();
|
||||||
|
} else if (typeof this._dataSourceConfig.shouldFetch === 'boolean') {
|
||||||
|
shouldFetch = this._dataSourceConfig.shouldFetch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldFetch) {
|
||||||
|
this._status = RuntimeDataSourceStatus.Error;
|
||||||
|
this._error = new Error(`the ${this._dataSourceConfig.id} request should not fetch, please check the condition`);
|
||||||
|
throw this._error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchOptions = this._options;
|
||||||
|
|
||||||
|
// willFetch
|
||||||
|
try {
|
||||||
|
fetchOptions = await this._dataSourceConfig.willFetch!(this._options);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 约定如果 params 有内容,直接做替换,如果没有就用默认的 options 的
|
||||||
|
if (params && fetchOptions) {
|
||||||
|
fetchOptions.params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataHandler = this._dataSourceConfig.dataHandler!;
|
||||||
|
const { errorHandler } = this._dataSourceConfig;
|
||||||
|
|
||||||
|
// 调用实际的请求,获取到对应的数据和状态后赋值给当前的 dataSource
|
||||||
|
try {
|
||||||
|
this._status = RuntimeDataSourceStatus.Loading;
|
||||||
|
|
||||||
|
// _context 会给传,但是用不用由 handler 说了算
|
||||||
|
const result = await (this._request as RequestHandler<{
|
||||||
|
data: TResultData;
|
||||||
|
}>)(fetchOptions, this._context).then(dataHandler, errorHandler);
|
||||||
|
|
||||||
|
// setState
|
||||||
|
this._context.setState({
|
||||||
|
[this._dataSourceConfig.id]: result,
|
||||||
|
});
|
||||||
|
// 结果赋值
|
||||||
|
this._data = result;
|
||||||
|
this._status = RuntimeDataSourceStatus.Loaded;
|
||||||
|
return this._data;
|
||||||
|
} catch (error) {
|
||||||
|
this._error = error;
|
||||||
|
this._status = RuntimeDataSourceStatus.Error;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RuntimeDataSourceItem };
|
||||||
47
packages/datasource-engine/src/core/adapter.ts
Normal file
47
packages/datasource-engine/src/core/adapter.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { getRuntimeValueFromConfig, getRuntimeJsValue, buildOptions, buildShouldFetch } from './../utils';
|
||||||
|
// 将不同渠道给的 schema 转为 runtime 需要的类型
|
||||||
|
|
||||||
|
import { defaultDataHandler, defaultWillFetch } from '../helpers';
|
||||||
|
import {
|
||||||
|
DataSourceMap,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
InterpretDataSource,
|
||||||
|
InterpretDataSourceConfig,
|
||||||
|
RuntimeDataSourceConfig,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
const adapt2Runtime = (dataSource: InterpretDataSource, context: IDataSourceRuntimeContext) => {
|
||||||
|
const { list: interpretConfigList, dataHandler: interpretDataHandler } = dataSource;
|
||||||
|
const dataHandler: (dataMap?: DataSourceMap) => void = interpretDataHandler
|
||||||
|
? getRuntimeJsValue(interpretDataHandler, context)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// 为空判断
|
||||||
|
if (!interpretConfigList || !interpretConfigList.length) {
|
||||||
|
return {
|
||||||
|
list: [],
|
||||||
|
dataHandler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const list: RuntimeDataSourceConfig[] = interpretConfigList.map((el: InterpretDataSourceConfig) => {
|
||||||
|
return {
|
||||||
|
id: el.id,
|
||||||
|
isInit: getRuntimeValueFromConfig('boolean', el.isInit, context) || true, // 默认 true
|
||||||
|
isSync: getRuntimeValueFromConfig('boolean', el.isSync, context) || false, // 默认 false
|
||||||
|
type: el.type || 'fetch',
|
||||||
|
willFetch: el.willFetch ? getRuntimeJsValue(el.willFetch, context) : defaultWillFetch,
|
||||||
|
shouldFetch: buildShouldFetch(el, context),
|
||||||
|
dataHandler: el.dataHandler ? getRuntimeJsValue(el.dataHandler, context) : defaultDataHandler,
|
||||||
|
errorHandler: el.errorHandler ? getRuntimeJsValue(el.errorHandler, context) : undefined,
|
||||||
|
requestHandler: el.requestHandler ? getRuntimeJsValue(el.requestHandler, context) : undefined,
|
||||||
|
options: buildOptions(el, context),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
dataHandler,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { adapt2Runtime };
|
||||||
@ -1 +1,2 @@
|
|||||||
export { create } from './DataSourceEngine';
|
export { RuntimeDataSourceItem } from './RuntimeDataSourceItem';
|
||||||
|
export { adapt2Runtime } from './adapter';
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { DataSourceMap, RuntimeDataSource, RuntimeDataSourceConfig } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
export const reloadDataSourceFactory = (
|
||||||
|
dataSource: RuntimeDataSource,
|
||||||
|
dataSourceMap: DataSourceMap,
|
||||||
|
dataHandler?: (dataSourceMap: DataSourceMap) => void,
|
||||||
|
) => async () => {
|
||||||
|
const allAsyncLoadings: Array<Promise<any>> = [];
|
||||||
|
|
||||||
|
// TODO: 那么,如果有新的类型过来,这个地方怎么处理???
|
||||||
|
// 单独处理 urlParams 类型的
|
||||||
|
dataSource.list
|
||||||
|
.filter(
|
||||||
|
(el: RuntimeDataSourceConfig) =>
|
||||||
|
// eslint-disable-next-line implicit-arrow-linebreak
|
||||||
|
el.type === 'urlParams' && (typeof el.isInit === 'boolean' ? el.isInit : true),
|
||||||
|
)
|
||||||
|
.forEach((el: RuntimeDataSourceConfig) => {
|
||||||
|
dataSourceMap[el.id].load();
|
||||||
|
});
|
||||||
|
|
||||||
|
const remainRuntimeDataSourceList = dataSource.list.filter((el: RuntimeDataSourceConfig) => el.type !== 'urlParams');
|
||||||
|
|
||||||
|
// 处理并行
|
||||||
|
for (const ds of remainRuntimeDataSourceList) {
|
||||||
|
if (!ds.options) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
// 需要考虑出码直接不传值的情况
|
||||||
|
ds.isInit &&
|
||||||
|
!ds.isSync
|
||||||
|
) {
|
||||||
|
allAsyncLoadings.push(dataSourceMap[ds.id].load());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理串行
|
||||||
|
for (const ds of remainRuntimeDataSourceList) {
|
||||||
|
if (!ds.options) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// 需要考虑出码直接不传值的情况
|
||||||
|
ds.isInit &&
|
||||||
|
ds.isSync
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await dataSourceMap[ds.id].load();
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: 这个错误直接吃掉?
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(allAsyncLoadings);
|
||||||
|
|
||||||
|
// 所有的初始化请求都结束之后,调用钩子函数
|
||||||
|
|
||||||
|
if (dataHandler) {
|
||||||
|
dataHandler(dataSourceMap);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import request from 'universal-request';
|
|
||||||
import type { AsObject, RequestOptions } from 'universal-request/lib/types';
|
|
||||||
|
|
||||||
import { DataSourceOptions, RequestHandler } from '../types';
|
|
||||||
|
|
||||||
const fetchHandler: RequestHandler = async ({
|
|
||||||
url,
|
|
||||||
uri,
|
|
||||||
data,
|
|
||||||
params,
|
|
||||||
...otherOptions
|
|
||||||
}: DataSourceOptions) => {
|
|
||||||
const reqOptions = {
|
|
||||||
url: ((url || uri) as unknown) as string,
|
|
||||||
data: ((data || params) as unknown) as AsObject,
|
|
||||||
...otherOptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await request(reqOptions as RequestOptions);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default fetchHandler;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import mtop from '@ali/universal-mtop';
|
|
||||||
import { RequestHandler } from '../types';
|
|
||||||
|
|
||||||
const mtopHandler: RequestHandler = async (options) => {
|
|
||||||
const { api, uri, data, params, type, method, ...otherOptions } = options;
|
|
||||||
const reqOptions = {
|
|
||||||
...otherOptions,
|
|
||||||
api: api || uri,
|
|
||||||
data: data || params,
|
|
||||||
type: type || method,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await mtop(reqOptions);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default mtopHandler;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import qs from 'query-string';
|
|
||||||
import { RequestHandler } from '../types';
|
|
||||||
|
|
||||||
export default function urlParamsHandler(search: string | Record<string, unknown>): RequestHandler {
|
|
||||||
const urlParams = typeof search === 'string' ? qs.parse(search) : search;
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
return {
|
|
||||||
data: urlParams,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
33
packages/datasource-engine/src/helpers/index.ts
Normal file
33
packages/datasource-engine/src/helpers/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
DataHandler,
|
||||||
|
RequestHandler,
|
||||||
|
RequestHandlersMap,
|
||||||
|
RuntimeDataSourceConfig,
|
||||||
|
RuntimeOptionsConfig,
|
||||||
|
UrlParamsHandler,
|
||||||
|
WillFetch,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 默认的 dataSourceItem 的 dataHandler
|
||||||
|
export const defaultDataHandler: DataHandler = async <T = unknown>(response: { data: T }) => response.data;
|
||||||
|
|
||||||
|
// 默认的 dataSourceItem 的 willFetch
|
||||||
|
export const defaultWillFetch: WillFetch = (options: RuntimeOptionsConfig) => options;
|
||||||
|
|
||||||
|
// 默认的 dataSourceItem 的 shouldFetch
|
||||||
|
export const defaultShouldFetch = () => true;
|
||||||
|
|
||||||
|
type GetRequestHandler<T = unknown> = (
|
||||||
|
ds: RuntimeDataSourceConfig,
|
||||||
|
requestHandlersMap: RequestHandlersMap<{ data: T }>,
|
||||||
|
) => RequestHandler<{ data: T }> | UrlParamsHandler<T>;
|
||||||
|
|
||||||
|
// 从当前 dataSourceItem 中获取 requestHandler
|
||||||
|
export const getRequestHandler: GetRequestHandler = (ds, requestHandlersMap) => {
|
||||||
|
if (ds.type === 'custom') {
|
||||||
|
// 自定义类型处理
|
||||||
|
return (ds.requestHandler as unknown) as RequestHandler<{ data: unknown }>; // 理论上这里应该是能强转的,就算为空,应该在 request 请求的时候触发失败
|
||||||
|
}
|
||||||
|
// type 协议默认值 fetch
|
||||||
|
return requestHandlersMap[ds.type || 'fetch'];
|
||||||
|
};
|
||||||
@ -1,2 +1,4 @@
|
|||||||
export * from './core';
|
import createInterpret from './interpret/DataSourceEngineFactory';
|
||||||
export * from './types';
|
import createRuntime from './runtime/RuntimeDataSourceEngineFactory';
|
||||||
|
|
||||||
|
export { createInterpret, createRuntime };
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { adapt2Runtime } from '../core/adapter';
|
||||||
|
import { RuntimeDataSourceItem } from '../core/RuntimeDataSourceItem';
|
||||||
|
import { reloadDataSourceFactory } from '../core/reloadDataSourceFactory';
|
||||||
|
import {
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
InterpretDataSource,
|
||||||
|
IRuntimeDataSource,
|
||||||
|
RequestHandlersMap,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceConfig,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import { getRequestHandler } from '../helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param dataSource
|
||||||
|
* @param context
|
||||||
|
* @param extraConfig: { requestHandlersMap }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default (
|
||||||
|
dataSource: InterpretDataSource,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
extraConfig: {
|
||||||
|
requestHandlersMap: RequestHandlersMap<{ data: unknown }>;
|
||||||
|
} = { requestHandlersMap: {} },
|
||||||
|
) => {
|
||||||
|
const { requestHandlersMap } = extraConfig;
|
||||||
|
|
||||||
|
const runtimeDataSource: RuntimeDataSource = adapt2Runtime(dataSource, context);
|
||||||
|
|
||||||
|
const dataSourceMap = runtimeDataSource.list.reduce(
|
||||||
|
(prev: Record<string, IRuntimeDataSource>, current: RuntimeDataSourceConfig) => {
|
||||||
|
prev[current.id] = new RuntimeDataSourceItem(current, getRequestHandler(current, requestHandlersMap), context);
|
||||||
|
return prev;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataSourceMap,
|
||||||
|
reloadDataSource: reloadDataSourceFactory(runtimeDataSource, dataSourceMap, runtimeDataSource.dataHandler),
|
||||||
|
};
|
||||||
|
};
|
||||||
3
packages/datasource-engine/src/interpret/index.ts
Normal file
3
packages/datasource-engine/src/interpret/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import createInterpret from './DataSourceEngineFactory';
|
||||||
|
|
||||||
|
export const create = createInterpret;
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/indent */
|
||||||
|
import {
|
||||||
|
IRuntimeDataSource,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RequestHandlersMap,
|
||||||
|
RuntimeDataSourceConfig,
|
||||||
|
RuntimeDataSource,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
import { RuntimeDataSourceItem } from '../core';
|
||||||
|
import { reloadDataSourceFactory } from '../core/reloadDataSourceFactory';
|
||||||
|
import { defaultDataHandler, defaultShouldFetch, defaultWillFetch, getRequestHandler } from '../helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param dataSource
|
||||||
|
* @param context
|
||||||
|
* @param extraConfig: { requestHandlersMap }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default (
|
||||||
|
dataSource: RuntimeDataSource,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
extraConfig: {
|
||||||
|
requestHandlersMap: RequestHandlersMap<{ data: unknown }>;
|
||||||
|
} = { requestHandlersMap: {} },
|
||||||
|
) => {
|
||||||
|
const { requestHandlersMap } = extraConfig;
|
||||||
|
|
||||||
|
// TODO: 对于出码类型,需要做一层数据兼容,给一些必要的值设置默认值,先兜底几个必要的
|
||||||
|
dataSource.list.forEach(ds => {
|
||||||
|
ds.isInit = ds.isInit || true;
|
||||||
|
ds.isSync = ds.isSync || false;
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
ds.shouldFetch = !ds.shouldFetch
|
||||||
|
? defaultShouldFetch
|
||||||
|
: typeof ds.shouldFetch === 'function'
|
||||||
|
? ds.shouldFetch.bind(context)
|
||||||
|
: ds.shouldFetch;
|
||||||
|
ds.willFetch = ds.willFetch ? ds.willFetch.bind(context) : defaultWillFetch;
|
||||||
|
ds.dataHandler = ds.dataHandler ? ds.dataHandler.bind(context) : defaultDataHandler;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSourceMap = dataSource.list.reduce(
|
||||||
|
(prev: Record<string, IRuntimeDataSource>, current: RuntimeDataSourceConfig) => {
|
||||||
|
prev[current.id] = new RuntimeDataSourceItem(current, getRequestHandler(current, requestHandlersMap), context);
|
||||||
|
return prev;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataSourceMap,
|
||||||
|
reloadDataSource: reloadDataSourceFactory(dataSource, dataSourceMap, dataSource.dataHandler),
|
||||||
|
};
|
||||||
|
};
|
||||||
3
packages/datasource-engine/src/runtime/index.ts
Normal file
3
packages/datasource-engine/src/runtime/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import createRuntime from './RuntimeDataSourceEngineFactory';
|
||||||
|
|
||||||
|
export const create = createRuntime;
|
||||||
3
packages/datasource-engine/src/typings.d.ts
vendored
Normal file
3
packages/datasource-engine/src/typings.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare module '@ali/mirror-io-client-mopen';
|
||||||
|
declare module '@ali/mirror-io-client-mtop';
|
||||||
|
declare module '@ali/mirror-io-client-universal-mtop';
|
||||||
193
packages/datasource-engine/src/utils.ts
Normal file
193
packages/datasource-engine/src/utils.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
/* eslint-disable no-new-func */
|
||||||
|
|
||||||
|
import {
|
||||||
|
CompositeValue,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
InterpretDataSourceConfig,
|
||||||
|
isJSExpression,
|
||||||
|
isJSFunction,
|
||||||
|
JSExpression,
|
||||||
|
JSFunction,
|
||||||
|
JSONObject,
|
||||||
|
RuntimeOptionsConfig,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
export const transformExpression = (
|
||||||
|
code: string,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return new Function(`return (${code})`).call(context);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`transformExpression error, code is ${code}, context is ${context}, error is ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformFunction = (
|
||||||
|
code: string,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return new Function(`return (${code})`).call(context).bind(context);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`transformFunction error, code is ${code}, context is ${context}, error is ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformBoolStr = (str: string) => {
|
||||||
|
return str !== 'false';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRuntimeJsValue = (
|
||||||
|
value: JSExpression | JSFunction,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) => {
|
||||||
|
if (!['JSExpression', 'JSFunction'].includes(value.type)) {
|
||||||
|
console.error(`translate error, value is ${JSON.stringify(value)}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// TODO: 类型修复
|
||||||
|
const code = value.compiled || value.value;
|
||||||
|
return value.type === 'JSFunction'
|
||||||
|
? transformFunction(code, context)
|
||||||
|
: transformExpression(code, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRuntimeBaseValue = (type: string, value: any) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
return `${value}`;
|
||||||
|
case 'boolean':
|
||||||
|
return typeof value === 'string'
|
||||||
|
? transformBoolStr(value as string)
|
||||||
|
: value;
|
||||||
|
case 'number':
|
||||||
|
return Number(value);
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRuntimeValueFromConfig = (
|
||||||
|
type: string,
|
||||||
|
value: CompositeValue,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) => {
|
||||||
|
if (!value) return undefined;
|
||||||
|
if (isJSExpression(value) || isJSFunction(value)) {
|
||||||
|
return getRuntimeBaseValue(type, getRuntimeJsValue(value, context));
|
||||||
|
}
|
||||||
|
return getRuntimeBaseValue(type, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildJsonObj = (
|
||||||
|
params: JSONObject | JSExpression,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) => {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
if (isJSExpression(params)) {
|
||||||
|
return transformExpression(params.value, context);
|
||||||
|
}
|
||||||
|
Object.keys(params).forEach((key: string) => {
|
||||||
|
const currentParam: any = params[key];
|
||||||
|
if (isJSExpression(currentParam)) {
|
||||||
|
result[key] = transformExpression(currentParam.value, context);
|
||||||
|
} else {
|
||||||
|
result[key] = getRuntimeBaseValue(currentParam.type, currentParam.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildShouldFetch = (
|
||||||
|
ds: InterpretDataSourceConfig,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) => {
|
||||||
|
if (!ds.options || !ds.shouldFetch) {
|
||||||
|
return true; // 默认为 true
|
||||||
|
}
|
||||||
|
if (isJSExpression(ds.shouldFetch) || isJSFunction(ds.shouldFetch)) {
|
||||||
|
return getRuntimeJsValue(ds.shouldFetch, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRuntimeBaseValue('boolean', ds.shouldFetch);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildOptions = (
|
||||||
|
ds: InterpretDataSourceConfig,
|
||||||
|
context: IDataSourceRuntimeContext,
|
||||||
|
) => {
|
||||||
|
const { options } = ds;
|
||||||
|
if (!options) return undefined;
|
||||||
|
// eslint-disable-next-line space-before-function-paren
|
||||||
|
return function() {
|
||||||
|
// 默认值
|
||||||
|
const fetchOptions: RuntimeOptionsConfig = {
|
||||||
|
uri: '',
|
||||||
|
params: {},
|
||||||
|
method: 'GET',
|
||||||
|
isCors: true,
|
||||||
|
timeout: 5000,
|
||||||
|
headers: undefined,
|
||||||
|
v: '1.0',
|
||||||
|
};
|
||||||
|
Object.keys(options).forEach((key: string) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'uri':
|
||||||
|
fetchOptions.uri = getRuntimeValueFromConfig(
|
||||||
|
'string',
|
||||||
|
options.uri,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'params':
|
||||||
|
fetchOptions.params = buildJsonObj(options.params!, context);
|
||||||
|
break;
|
||||||
|
case 'method':
|
||||||
|
fetchOptions.method = getRuntimeValueFromConfig(
|
||||||
|
'string',
|
||||||
|
options.method,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'isCors':
|
||||||
|
fetchOptions.isCors = getRuntimeValueFromConfig(
|
||||||
|
'boolean',
|
||||||
|
options.isCors,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'timeout':
|
||||||
|
fetchOptions.timeout = getRuntimeValueFromConfig(
|
||||||
|
'number',
|
||||||
|
options.timeout,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'headers':
|
||||||
|
fetchOptions.headers = buildJsonObj(options.headers!, context);
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
fetchOptions.v = getRuntimeValueFromConfig(
|
||||||
|
'string',
|
||||||
|
options.v,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 其余的除了做表达式或者 function 的转换,直接透传
|
||||||
|
fetchOptions[key] = getRuntimeValueFromConfig(
|
||||||
|
'unknown',
|
||||||
|
options[key],
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return fetchOptions;
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
export function bindRuntimeContext<T, U>(x: T, ctx: U): T {
|
||||||
|
if (typeof x === 'function') {
|
||||||
|
return x.bind(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof x !== 'object') {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(x)) {
|
||||||
|
return (x.map((item) => bindRuntimeContext(item, ctx)) as unknown) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = {} as Record<string, unknown>;
|
||||||
|
|
||||||
|
Object.entries(x).forEach(([k, v]) => {
|
||||||
|
res[k] = bindRuntimeContext(v, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (res as unknown) as T;
|
||||||
|
}
|
||||||
3
packages/datasource-engine/test/_helpers/delay.ts
Normal file
3
packages/datasource-engine/test/_helpers/delay.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export async function delay(ms: number = 0) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
3
packages/datasource-engine/test/_helpers/index.ts
Normal file
3
packages/datasource-engine/test/_helpers/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './mock-context';
|
||||||
|
export * from './delay';
|
||||||
|
export * from './bind-runtime-context';
|
||||||
53
packages/datasource-engine/test/_helpers/mock-context.ts
Normal file
53
packages/datasource-engine/test/_helpers/mock-context.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
IDataSourceEngine
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
export class MockContext<TState extends Record<string, unknown> = Record<string, unknown>>
|
||||||
|
implements IDataSourceRuntimeContext<TState> {
|
||||||
|
private _dataSourceEngine: IDataSourceEngine;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private _state: TState,
|
||||||
|
private _createDataSourceEngine: (
|
||||||
|
context: IDataSourceRuntimeContext<TState>
|
||||||
|
) => IDataSourceEngine,
|
||||||
|
private _customMethods: Record<string, () => any> = {}
|
||||||
|
) {
|
||||||
|
this._dataSourceEngine = _createDataSourceEngine(this);
|
||||||
|
|
||||||
|
// 自定义方法
|
||||||
|
Object.assign(this, _customMethods);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get state() {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setState(state: Partial<TState>) {
|
||||||
|
this._state = {
|
||||||
|
...this._state,
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public get dataSourceMap() {
|
||||||
|
return this._dataSourceEngine.dataSourceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reloadDataSource(): Promise<void> {
|
||||||
|
this._dataSourceEngine.reloadDataSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get page(): any {
|
||||||
|
throw new Error('this.page should not be accessed by datasource-engine');
|
||||||
|
}
|
||||||
|
|
||||||
|
public get component(): any {
|
||||||
|
throw new Error(
|
||||||
|
'this.component should not be accessed by datasource-engine'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[customMethod: string]: any;
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# 关于此场景
|
||||||
|
|
||||||
|
数据源的 type 可以是 `custom` 类型的, 此时需要提供 `requestHandler` 给数据源
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { RuntimeDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const dataSource: RuntimeDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'custom',
|
||||||
|
isSync: true,
|
||||||
|
requestHandler: options => {
|
||||||
|
return new Promise(res => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// test return data
|
||||||
|
res({
|
||||||
|
data: {
|
||||||
|
id: 9527,
|
||||||
|
name: 'Alice',
|
||||||
|
uri: options.uri,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
options() {
|
||||||
|
return {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orders',
|
||||||
|
isInit: true,
|
||||||
|
type: 'custom',
|
||||||
|
isSync: true,
|
||||||
|
requestHandler: () => {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// test return data
|
||||||
|
rej(new Error('test error'));
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
options() {
|
||||||
|
return {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/orders.json',
|
||||||
|
params: {
|
||||||
|
userId: this.state.user.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 这个 api 是假的,调不通的,当前场景是故意需要报错的
|
||||||
|
id: 'members',
|
||||||
|
isInit: true,
|
||||||
|
type: 'custom',
|
||||||
|
isSync: true,
|
||||||
|
options() {
|
||||||
|
return {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/members.json',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { InterpretDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的 schema:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const DATA_SOURCE_SCHEMA: InterpretDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'custom',
|
||||||
|
isSync: true,
|
||||||
|
requestHandler: {
|
||||||
|
type: 'JSFunction',
|
||||||
|
value: `function(options){
|
||||||
|
return new Promise(res => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// test return data
|
||||||
|
res({
|
||||||
|
data: {
|
||||||
|
id: 9527,
|
||||||
|
name: 'Alice',
|
||||||
|
uri: options.uri,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orders',
|
||||||
|
isInit: true,
|
||||||
|
type: 'custom',
|
||||||
|
isSync: true,
|
||||||
|
requestHandler: {
|
||||||
|
type: 'JSFunction',
|
||||||
|
value: `function(options){
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// test return data
|
||||||
|
return rej(new Error('test error'));
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/orders.json',
|
||||||
|
params: {
|
||||||
|
type: 'JSExpression',
|
||||||
|
value: '{ userId: this.state.user.id }',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'members',
|
||||||
|
isInit: true,
|
||||||
|
type: 'custom',
|
||||||
|
isSync: true,
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/members.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { bindRuntimeContext, MockContext } from '../../_helpers';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const normalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options?: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const USER_DATA = {
|
||||||
|
id: 9527,
|
||||||
|
name: 'Alice',
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json'
|
||||||
|
};
|
||||||
|
const ERROR_MSG = 'test error';
|
||||||
|
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(bindRuntimeContext(dataSource, ctx), ctx));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
t.is(context.dataSourceMap.members.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await clock.tickAsync(1050);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await clock.tickAsync(1050);
|
||||||
|
|
||||||
|
// members 因为没有 requestHandler 直接就挂了
|
||||||
|
t.is(context.dataSourceMap.members.status, RuntimeDataSourceStatus.Error)
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后 user 应该成功了,loaded
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
// 最后 orders 应该失败了,error 状态
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Error);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, USER_DATA);
|
||||||
|
t.is(context.dataSourceMap.user.error, undefined);
|
||||||
|
t.deepEqual(context.dataSourceMap.orders.data, undefined);
|
||||||
|
t.not(context.dataSourceMap.orders.error, undefined);
|
||||||
|
t.deepEqual(context.dataSourceMap.members.data, undefined);
|
||||||
|
t.not(context.dataSourceMap.members.error, undefined);
|
||||||
|
t.regex(context.dataSourceMap.orders.error!.message, new RegExp(ERROR_MSG));
|
||||||
|
t.regex(context.dataSourceMap.members.error!.message, new RegExp('no custom handler provide'));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledOnce);
|
||||||
|
t.deepEqual(context.state.user, USER_DATA);
|
||||||
|
t.is(context.state.orders, undefined);
|
||||||
|
t.is(context.state.members, undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
normalScene.title = (providedTitle) => providedTitle || 'normal scene';
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# 关于此场景
|
||||||
|
|
||||||
|
这个是一个及其简单的场景 -- 就是直接调用 fetch,没有啥 dataHandler 之类的
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { RuntimeDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const dataSource: RuntimeDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
type: 'fetch',
|
||||||
|
options: () => ({
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { InterpretDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的 schema:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const DATA_SOURCE_SCHEMA: InterpretDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
type: 'fetch',
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { delay, MockContext } from '../../_helpers';
|
||||||
|
// import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const abnormalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
const ERROR_MSG = 'test error';
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
throw new Error(ERROR_MSG);
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(dataSource, ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该失败了,error 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Error);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, undefined);
|
||||||
|
t.not(context.dataSourceMap.user.error, undefined);
|
||||||
|
t.regex(context.dataSourceMap.user.error!.message, new RegExp(ERROR_MSG));
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.notCalled);
|
||||||
|
t.deepEqual(context.state.user, undefined);
|
||||||
|
|
||||||
|
// fetchHandler 不应该被调
|
||||||
|
t.assert(fetchHandler.calledOnce);
|
||||||
|
};
|
||||||
|
|
||||||
|
abnormalScene.title = (providedTitle) => providedTitle || 'abnormal scene';
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const normalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const USER_DATA = {
|
||||||
|
name: 'Alice',
|
||||||
|
age: 18,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
return { data: USER_DATA };
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(dataSource, ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该成功了,loaded 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, USER_DATA);
|
||||||
|
t.deepEqual(context.dataSourceMap.user.error, undefined);
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledOnce);
|
||||||
|
t.deepEqual(context.state.user, USER_DATA);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了一次
|
||||||
|
t.assert(fetchHandler.calledOnce);
|
||||||
|
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
// 检查调用参数
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
normalScene.title = (providedTitle) => providedTitle || 'normal scene';
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# 关于此场景
|
||||||
|
|
||||||
|
有些场景下,多个数据源之间有依赖关系,这时候可以将其都设置为 `isSync: true`, 这样这些数据源就会按配置面板的顺序进行串行调用。
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { RuntimeDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const dataSource: RuntimeDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
isSync: true,
|
||||||
|
options() {
|
||||||
|
return {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orders',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
isSync: true,
|
||||||
|
options() {
|
||||||
|
return {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/orders.json',
|
||||||
|
params: {
|
||||||
|
userId: this.state.user.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { InterpretDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的 schema:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const DATA_SOURCE_SCHEMA: InterpretDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
isSync: true,
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orders',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
isSync: true,
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/orders.json',
|
||||||
|
params: {
|
||||||
|
type: 'JSExpression',
|
||||||
|
value: '{ userId: this.state.user.id }',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { bindRuntimeContext, delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const abnormalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const USER_DATA = {
|
||||||
|
id: 9527,
|
||||||
|
name: 'Alice',
|
||||||
|
};
|
||||||
|
const ERROR_MSG = 'test error';
|
||||||
|
const fetchHandler = sinon.fake(async ({ uri }) => {
|
||||||
|
await delay(100);
|
||||||
|
if (/user/.test(uri)) {
|
||||||
|
return { data: USER_DATA };
|
||||||
|
} else {
|
||||||
|
throw new Error(ERROR_MSG);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(bindRuntimeContext(dataSource, ctx), ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后 user 应该成功了,loaded
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
// 最后 orders 应该失败了,error 状态
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Error);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, USER_DATA);
|
||||||
|
t.is(context.dataSourceMap.user.error, undefined);
|
||||||
|
t.deepEqual(context.dataSourceMap.orders.data, undefined);
|
||||||
|
t.not(context.dataSourceMap.orders.error, undefined);
|
||||||
|
t.regex(context.dataSourceMap.orders.error!.message, new RegExp(ERROR_MSG));
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledOnce);
|
||||||
|
t.deepEqual(context.state.user, USER_DATA);
|
||||||
|
t.is(context.state.orders, undefined);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了2次
|
||||||
|
t.assert(fetchHandler.calledTwice);
|
||||||
|
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
// 检查调用参数
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
abnormalScene.title = (providedTitle) => providedTitle || 'abnormal scene';
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { bindRuntimeContext, delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const normalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const USER_DATA = {
|
||||||
|
id: 9527,
|
||||||
|
name: 'Alice',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ORDERS_DATA = [{ id: 123 }, { id: 456 }];
|
||||||
|
|
||||||
|
const fetchHandler = sinon.fake(async ({ uri }) => {
|
||||||
|
await delay(100);
|
||||||
|
return { data: /user/.test(uri) ? USER_DATA : ORDERS_DATA };
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(bindRuntimeContext(dataSource, ctx), ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该成功了,loaded 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
t.is(context.dataSourceMap.orders.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, USER_DATA);
|
||||||
|
t.deepEqual(context.dataSourceMap.user.error, undefined);
|
||||||
|
t.deepEqual(context.dataSourceMap.orders.data, ORDERS_DATA);
|
||||||
|
t.deepEqual(context.dataSourceMap.orders.error, undefined);
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledTwice);
|
||||||
|
t.deepEqual(context.state.user, USER_DATA);
|
||||||
|
t.deepEqual(context.state.orders, ORDERS_DATA);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了2次
|
||||||
|
t.assert(fetchHandler.calledTwice);
|
||||||
|
|
||||||
|
// 检查调用参数
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
normalScene.title = (providedTitle) => providedTitle || 'normal scene';
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# 关于此场景
|
||||||
|
|
||||||
|
这个是一个及其简单的场景 -- 就是直接调用 fetch,没有啥 dataHandler 之类的
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { RuntimeDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const dataSource: RuntimeDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'urlParams',
|
||||||
|
isInit: true,
|
||||||
|
type: 'urlParams',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { InterpretDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的 schema:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const DATA_SOURCE_SCHEMA: InterpretDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'urlParams',
|
||||||
|
isInit: true,
|
||||||
|
type: 'urlParams',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { MockContext } from '../../_helpers';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const normalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const URL_PARAMS = {
|
||||||
|
name: 'Alice',
|
||||||
|
age: '18',
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlParamsHandler = sinon.fake(async () => {
|
||||||
|
return URL_PARAMS; // TODO: 别的都是返回的套了一层 data 的,但是 urlParams 的为啥不一样?
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(dataSource, ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
urlParams: urlParamsHandler,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.urlParams.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该成功了,loaded 状态
|
||||||
|
t.is(context.dataSourceMap.urlParams.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.urlParams.data, URL_PARAMS);
|
||||||
|
t.deepEqual(context.dataSourceMap.urlParams.error, undefined);
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledOnce);
|
||||||
|
t.deepEqual(context.state.urlParams, URL_PARAMS);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了一次
|
||||||
|
t.assert(urlParamsHandler.calledOnce);
|
||||||
|
|
||||||
|
// 检查调用参数 url 没有 options
|
||||||
|
t.deepEqual(urlParamsHandler.firstCall.args, [context]);
|
||||||
|
};
|
||||||
|
|
||||||
|
normalScene.title = (providedTitle) => providedTitle || 'normal scene';
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
# 关于此场景
|
||||||
|
|
||||||
|
这个是一个很常见的场景 -- 查出来的数据里面套的还有一层数据,可能有异常状态得需要处理下。
|
||||||
|
|
||||||
|
比如,期望的正常的数据应该是:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
而异常场景下,服务端会返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "这是错误原因",
|
||||||
|
"code": "错误码"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
-- 这时候期望有异常监控埋点。
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { RuntimeDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const dataSource: RuntimeDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
options: () => ({
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
}),
|
||||||
|
dataHandler: function dataHandler(response: any) {
|
||||||
|
const { data } = response;
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('empty data!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordError({ type: 'FETCH_ERROR', detail: data });
|
||||||
|
|
||||||
|
throw new Error(data.message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { InterpretDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的 schema:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const DATA_SOURCE_SCHEMA: InterpretDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
},
|
||||||
|
dataHandler: {
|
||||||
|
type: 'JSFunction',
|
||||||
|
value: `
|
||||||
|
function dataHandler(response){
|
||||||
|
const { data } = response;
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('empty data!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success){
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordError({ type: 'FETCH_ERROR', detail: data });
|
||||||
|
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { bindRuntimeContext, delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const abnormalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
const ERROR_MSG = 'test error';
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
success: false,
|
||||||
|
message: ERROR_MSG,
|
||||||
|
code: 'E_FOO',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>(
|
||||||
|
{},
|
||||||
|
(ctx) => create(bindRuntimeContext(dataSource, ctx), ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
recordError() { },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
const recordError = sinon.spy(context, 'recordError');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该失败了,error 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Error);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, undefined);
|
||||||
|
t.not(context.dataSourceMap.user.error, undefined);
|
||||||
|
|
||||||
|
t.regex(context.dataSourceMap.user.error!.message, new RegExp(ERROR_MSG));
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.notCalled);
|
||||||
|
t.deepEqual(context.state.user, undefined);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了一次
|
||||||
|
t.assert(fetchHandler.calledOnce);
|
||||||
|
|
||||||
|
// 检查调用参数
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
|
||||||
|
// 埋点应该也会被调用
|
||||||
|
t.assert(recordError.calledOnce);
|
||||||
|
t.snapshot(recordError.firstCall.args);
|
||||||
|
};
|
||||||
|
|
||||||
|
abnormalScene.title = (providedTitle) => providedTitle || 'abnormal scene';
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { bindRuntimeContext, delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const normalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const USER_DATA = {
|
||||||
|
name: 'Alice',
|
||||||
|
age: 18,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
data: USER_DATA,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>(
|
||||||
|
{},
|
||||||
|
(ctx) => create(bindRuntimeContext(dataSource, ctx), ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
recordError() { },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
const recordError = sinon.spy(context, 'recordError');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该成功了,loaded 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, USER_DATA);
|
||||||
|
t.deepEqual(context.dataSourceMap.user.error, undefined);
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledOnce);
|
||||||
|
t.deepEqual(context.state.user, USER_DATA);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了一次
|
||||||
|
t.assert(fetchHandler.calledOnce);
|
||||||
|
|
||||||
|
// 检查调用参数
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
|
||||||
|
// 埋点不应该被调用
|
||||||
|
t.assert(recordError.notCalled);
|
||||||
|
};
|
||||||
|
|
||||||
|
normalScene.title = (providedTitle) => providedTitle || 'normal scene';
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
# Snapshot report for `test/scenes/custom-response-status/abnormal-interpret.test.ts`
|
||||||
|
|
||||||
|
The actual snapshot is saved in `abnormal-interpret.test.ts.snap`.
|
||||||
|
|
||||||
|
Generated by [AVA](https://avajs.dev).
|
||||||
|
|
||||||
|
## abnormal scene
|
||||||
|
|
||||||
|
> Snapshot 1
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
code: 'E_FOO',
|
||||||
|
message: 'test error',
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
|
type: 'FETCH_ERROR',
|
||||||
|
},
|
||||||
|
]
|
||||||
Binary file not shown.
@ -0,0 +1,20 @@
|
|||||||
|
# Snapshot report for `test/scenes/p0-1-custom-response-status/abnormal-runtime.test.ts`
|
||||||
|
|
||||||
|
The actual snapshot is saved in `abnormal-runtime.test.ts.snap`.
|
||||||
|
|
||||||
|
Generated by [AVA](https://avajs.dev).
|
||||||
|
|
||||||
|
## abnormal scene
|
||||||
|
|
||||||
|
> Snapshot 1
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
code: 'E_FOO',
|
||||||
|
message: 'test error',
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
|
type: 'FETCH_ERROR',
|
||||||
|
},
|
||||||
|
]
|
||||||
Binary file not shown.
@ -0,0 +1,3 @@
|
|||||||
|
# 关于此场景
|
||||||
|
|
||||||
|
有些数据源的错误可以忽略(吃掉)-- 通过 dataHandler 捕获 error,只要其不重新抛出 error 而且不返回 rejected 状态的 Promise.
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { RuntimeDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
export const DEFAULT_USER_DATA = { id: 0, name: 'guest' }; // 返回一个兜底的数据
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const dataSource: RuntimeDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
options: () => ({
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
}),
|
||||||
|
dataHandler: function dataHandler(response: any) {
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { InterpretDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的 schema:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const DATA_SOURCE_SCHEMA: InterpretDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
},
|
||||||
|
dataHandler: {
|
||||||
|
type: 'JSFunction',
|
||||||
|
value: `
|
||||||
|
function dataHandler(response) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const abnormalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
const ERROR_MSG = 'test error';
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
throw new Error(ERROR_MSG);
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(dataSource, ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 注意 error 是会被吃掉了,还是 loaded 状态
|
||||||
|
// FIXME: 根据协议内容,dataHandler 返回的结果是需要抛出错误的,那么 fetchHandler 的错误难道不需要处理?
|
||||||
|
// TODO: 提案:request 如果挂了,不应该需要走 dataHandler 了,没有意义
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Error);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, undefined);
|
||||||
|
t.not(context.dataSourceMap.user.error, undefined);
|
||||||
|
t.regex(context.dataSourceMap.user.error!.message, new RegExp(ERROR_MSG));
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.notCalled);
|
||||||
|
|
||||||
|
// fetchHandler 应该没调
|
||||||
|
t.assert.skip(fetchHandler.notCalled);
|
||||||
|
|
||||||
|
// 检查调用参数
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
abnormalScene.title = (providedTitle) => providedTitle || 'abnormal scene';
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const normalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const USER_DATA = {
|
||||||
|
name: 'Alice',
|
||||||
|
age: 18,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
return { data: USER_DATA };
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>({}, (ctx) => create(dataSource, ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该成功了,loaded 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, USER_DATA);
|
||||||
|
t.deepEqual(context.dataSourceMap.user.error, undefined);
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledOnce);
|
||||||
|
t.deepEqual(context.state.user, USER_DATA);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了一次
|
||||||
|
t.assert(fetchHandler.calledOnce);
|
||||||
|
|
||||||
|
// 检查调用参数
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
normalScene.title = (providedTitle) => providedTitle || 'normal scene';
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# 关于此场景
|
||||||
|
|
||||||
|
某些场景下 dataHandler 不能同步返回,比如可能需要读取某个特殊的异步的数据源并合并响应数据。
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { RuntimeDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const dataSource: RuntimeDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
options: () => ({
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
}),
|
||||||
|
dataHandler: async function dataHandler(response: any) {
|
||||||
|
const { data } = response;
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('empty data!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordError({ type: 'FETCH_ERROR', detail: data });
|
||||||
|
|
||||||
|
throw new Error(data.message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { InterpretDataSource } from '@ali/lowcode-types';
|
||||||
|
|
||||||
|
// 这里仅仅是数据源部分的 schema:
|
||||||
|
// @see: https://yuque.antfin-inc.com/mo/spec/spec-low-code-building-schema#XMeF5
|
||||||
|
export const DATA_SOURCE_SCHEMA: InterpretDataSource = {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
isInit: true,
|
||||||
|
type: 'fetch',
|
||||||
|
options: {
|
||||||
|
uri: 'https://mocks.alibaba-inc.com/user.json',
|
||||||
|
},
|
||||||
|
dataHandler: {
|
||||||
|
type: 'JSFunction',
|
||||||
|
value: `
|
||||||
|
async function dataHandler(response){
|
||||||
|
const { data } = response;
|
||||||
|
if (!data) {
|
||||||
|
throw new Error('empty data!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success){
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordError({ type: 'FETCH_ERROR', detail: data });
|
||||||
|
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { bindRuntimeContext, delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const abnormalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
const ERROR_MSG = 'test error';
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
success: false,
|
||||||
|
message: ERROR_MSG,
|
||||||
|
code: 'E_FOO',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>(
|
||||||
|
{},
|
||||||
|
(ctx) => create(bindRuntimeContext(dataSource, ctx), ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
recordError() { },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
const recordError = sinon.spy(context, 'recordError');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该失败了,error 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Error);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, undefined);
|
||||||
|
t.not(context.dataSourceMap.user.error, undefined);
|
||||||
|
|
||||||
|
t.regex(context.dataSourceMap.user.error!.message, new RegExp(ERROR_MSG));
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.notCalled);
|
||||||
|
t.deepEqual(context.state.user, undefined);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了一次
|
||||||
|
t.assert(fetchHandler.calledOnce);
|
||||||
|
|
||||||
|
// 检查调用参数
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
|
||||||
|
// 埋点应该也会被调用
|
||||||
|
t.assert(recordError.calledOnce);
|
||||||
|
t.snapshot(recordError.firstCall.args);
|
||||||
|
};
|
||||||
|
|
||||||
|
abnormalScene.title = (providedTitle) => providedTitle || 'abnormal scene';
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
InterpretDataSource,
|
||||||
|
IDataSourceEngine,
|
||||||
|
IDataSourceRuntimeContext,
|
||||||
|
RuntimeDataSource,
|
||||||
|
RuntimeDataSourceStatus,
|
||||||
|
} from '@ali/lowcode-types';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { bindRuntimeContext, delay, MockContext } from '../../_helpers';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
|
||||||
|
import type { ExecutionContext, Macro } from 'ava';
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
export const normalScene: Macro<[
|
||||||
|
{
|
||||||
|
create: (
|
||||||
|
dataSource: any,
|
||||||
|
ctx: IDataSourceRuntimeContext,
|
||||||
|
options: any
|
||||||
|
) => IDataSourceEngine;
|
||||||
|
dataSource: RuntimeDataSource | InterpretDataSource;
|
||||||
|
}
|
||||||
|
]> = async (
|
||||||
|
t: ExecutionContext<{ clock: SinonFakeTimers }>,
|
||||||
|
{ create, dataSource },
|
||||||
|
) => {
|
||||||
|
const { clock } = t.context;
|
||||||
|
|
||||||
|
const USER_DATA = {
|
||||||
|
name: 'Alice',
|
||||||
|
age: 18,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchHandler = sinon.fake(async () => {
|
||||||
|
await delay(100);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
data: USER_DATA,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = new MockContext<Record<string, unknown>>(
|
||||||
|
{},
|
||||||
|
(ctx) => create(bindRuntimeContext(dataSource, ctx), ctx, {
|
||||||
|
requestHandlersMap: {
|
||||||
|
fetch: fetchHandler,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
recordError() { },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setState = sinon.spy(context, 'setState');
|
||||||
|
const recordError = sinon.spy(context, 'recordError');
|
||||||
|
|
||||||
|
// 一开始应该是初始状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Initial);
|
||||||
|
|
||||||
|
const loading = context.reloadDataSource();
|
||||||
|
|
||||||
|
await clock.tickAsync(50);
|
||||||
|
|
||||||
|
// 中间应该有 loading 态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loading);
|
||||||
|
|
||||||
|
await Promise.all([clock.runAllAsync(), loading]);
|
||||||
|
|
||||||
|
// 最后应该成功了,loaded 状态
|
||||||
|
t.is(context.dataSourceMap.user.status, RuntimeDataSourceStatus.Loaded);
|
||||||
|
|
||||||
|
// 检查数据源的数据
|
||||||
|
t.deepEqual(context.dataSourceMap.user.data, USER_DATA);
|
||||||
|
t.deepEqual(context.dataSourceMap.user.error, undefined);
|
||||||
|
|
||||||
|
// 检查状态数据
|
||||||
|
t.assert(setState.calledOnce);
|
||||||
|
t.deepEqual(context.state.user, USER_DATA);
|
||||||
|
|
||||||
|
// fetchHandler 应该被调用了一次
|
||||||
|
t.assert(fetchHandler.calledOnce);
|
||||||
|
|
||||||
|
// 检查调用参数
|
||||||
|
const firstListItemOptions = DATA_SOURCE_SCHEMA.list[0].options;
|
||||||
|
const fetchHandlerCallArgs = fetchHandler.firstCall.args[0];
|
||||||
|
t.is(firstListItemOptions.uri, fetchHandlerCallArgs.uri);
|
||||||
|
|
||||||
|
// 埋点不应该被调用
|
||||||
|
t.assert(recordError.notCalled);
|
||||||
|
};
|
||||||
|
|
||||||
|
normalScene.title = (providedTitle) => providedTitle || 'normal scene';
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
import type { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { abnormalScene } from './_macro-abnormal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(abnormalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/interpret';
|
||||||
|
|
||||||
|
import { DATA_SOURCE_SCHEMA } from './_datasource-schema';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource: DATA_SOURCE_SCHEMA,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import test, { ExecutionContext } from 'ava';
|
||||||
|
import sinon, { SinonFakeTimers } from 'sinon';
|
||||||
|
|
||||||
|
import { create } from '../../../src/runtime';
|
||||||
|
|
||||||
|
import { dataSource } from './_datasource-runtime';
|
||||||
|
import { normalScene } from './_macro-normal';
|
||||||
|
|
||||||
|
test.before((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after((t: ExecutionContext<{ clock: SinonFakeTimers }>) => {
|
||||||
|
t.context.clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(normalScene, {
|
||||||
|
create,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
# Snapshot report for `test/scenes/data-handler-returns-promise/abnormal-interpret.test.ts`
|
||||||
|
|
||||||
|
The actual snapshot is saved in `abnormal-interpret.test.ts.snap`.
|
||||||
|
|
||||||
|
Generated by [AVA](https://avajs.dev).
|
||||||
|
|
||||||
|
## abnormal scene
|
||||||
|
|
||||||
|
> Snapshot 1
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
code: 'E_FOO',
|
||||||
|
message: 'test error',
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
|
type: 'FETCH_ERROR',
|
||||||
|
},
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user