feat: update datasource engine

This commit is contained in:
guokai.jgk 2020-10-29 17:54:53 +08:00
parent 4ef1097638
commit cf3c7dbd35
16 changed files with 355 additions and 117 deletions

View File

@ -3,5 +3,6 @@ module.exports = {
rules: {
'@typescript-eslint/no-parameter-properties': 0,
'no-param-reassign': 0,
'max-len': 0,
},
};

View File

@ -1,4 +1,5 @@
module.exports = {
singleQuote: true,
trailingComma: 'all',
printWidth: 120,
};

View File

@ -9,10 +9,8 @@ import {
UrlParamsHandler,
} from '@ali/lowcode-types';
class RuntimeDataSourceItem<
TParams extends Record<string, unknown> = Record<string, unknown>,
TResultData = unknown
> implements IRuntimeDataSource<TParams, TResultData> {
class RuntimeDataSourceItem<TParams extends Record<string, unknown> = Record<string, unknown>, TResultData = unknown>
implements IRuntimeDataSource<TParams, TResultData> {
private _data?: TResultData;
private _error?: Error;
@ -21,9 +19,7 @@ class RuntimeDataSourceItem<
private _dataSourceConfig: RuntimeDataSourceConfig;
private _request:
| RequestHandler<{ data: TResultData }>
| UrlParamsHandler<TResultData>;
private _request: RequestHandler<{ data: TResultData }> | UrlParamsHandler<TResultData>;
private _context: IDataSourceRuntimeContext;
@ -31,9 +27,7 @@ class RuntimeDataSourceItem<
constructor(
dataSourceConfig: RuntimeDataSourceConfig,
request:
| RequestHandler<{ data: TResultData }>
| UrlParamsHandler<TResultData>,
request: RequestHandler<{ data: TResultData }> | UrlParamsHandler<TResultData>,
context: IDataSourceRuntimeContext,
) {
this._dataSourceConfig = dataSourceConfig;
@ -57,14 +51,14 @@ class RuntimeDataSourceItem<
if (!this._dataSourceConfig) return;
// 考虑没有绑定对应的 handler 的情况
if (!this._request) {
throw new Error(`no ${this._dataSourceConfig.type} handler provide`);
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,
);
const response = await (this._request as UrlParamsHandler<TResultData>)(this._context);
this._context.setState({
[this._dataSourceConfig.id]: response,
});
@ -100,10 +94,8 @@ class RuntimeDataSourceItem<
if (!shouldFetch) {
this._status = RuntimeDataSourceStatus.Error;
this._error = new Error(
`the ${this._dataSourceConfig.id} request should not fetch, please check the condition`,
);
return;
this._error = new Error(`the ${this._dataSourceConfig.id} request should not fetch, please check the condition`);
throw this._error;
}
let fetchOptions = this._options;

View File

@ -1,10 +1,4 @@
import {
transformFunction,
getRuntimeValueFromConfig,
getRuntimeJsValue,
buildOptions,
buildShouldFetch,
} from './../utils';
import { getRuntimeValueFromConfig, getRuntimeJsValue, buildOptions, buildShouldFetch } from './../utils';
// 将不同渠道给的 schema 转为 runtime 需要的类型
import { defaultDataHandler, defaultWillFetch } from '../helpers';
@ -16,18 +10,11 @@ import {
RuntimeDataSourceConfig,
} from '@ali/lowcode-types';
const adapt2Runtime = (
dataSource: InterpretDataSource,
context: IDataSourceRuntimeContext,
) => {
const {
list: interpretConfigList,
dataHandler: interpretDataHandler,
} = dataSource;
const dataHandler: (dataMap?: DataSourceMap) => void =
interpretDataHandler &&
interpretDataHandler.compiled &&
transformFunction(interpretDataHandler.compiled, context);
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) {
@ -36,29 +23,20 @@ const adapt2Runtime = (
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,
options: buildOptions(el, context),
};
},
);
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,

View File

@ -1,12 +1,9 @@
import {
DataSourceMap,
RuntimeDataSource,
RuntimeDataSourceConfig,
} from '@ali/lowcode-types';
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>> = [];
@ -16,16 +13,13 @@ export const reloadDataSourceFactory = (
.filter(
(el: RuntimeDataSourceConfig) =>
// eslint-disable-next-line implicit-arrow-linebreak
el.type === 'urlParams' &&
(typeof el.isInit === 'boolean' ? el.isInit : true),
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',
);
const remainRuntimeDataSourceList = dataSource.list.filter((el: RuntimeDataSourceConfig) => el.type !== 'urlParams');
// 处理并行
for (const ds of remainRuntimeDataSourceList) {
@ -63,4 +57,10 @@ export const reloadDataSourceFactory = (
}
await Promise.allSettled(allAsyncLoadings);
// 所有的初始化请求都结束之后,调用钩子函数
if (dataHandler) {
dataHandler(dataSourceMap);
}
};

View File

@ -1,17 +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;
export const defaultDataHandler: DataHandler = async <T = unknown>(response: { data: T }) => response.data;
// 默认的 dataSourceItem 的 willFetch
export const defaultWillFetch: WillFetch = (options: RuntimeOptionsConfig) =>
options;
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'];
};

View File

@ -9,11 +9,12 @@ import {
RuntimeDataSource,
RuntimeDataSourceConfig,
} from '@ali/lowcode-types';
import { getRequestHandler } from '../helpers';
// TODO: requestConfig mtop 默认的请求 config 怎么处理?
/**
* @param dataSource
* @param context
* @param extraConfig: { requestHandlersMap }
*/
export default (
@ -25,22 +26,11 @@ export default (
) => {
const { requestHandlersMap } = extraConfig;
const runtimeDataSource: RuntimeDataSource = adapt2Runtime(
dataSource,
context,
);
const runtimeDataSource: RuntimeDataSource = adapt2Runtime(dataSource, context);
const dataSourceMap = runtimeDataSource.list.reduce(
(
prev: Record<string, IRuntimeDataSource>,
current: RuntimeDataSourceConfig,
) => {
prev[current.id] = new RuntimeDataSourceItem(
current,
// type 协议默认值 fetch
requestHandlersMap[current.type || 'fetch'],
context,
);
(prev: Record<string, IRuntimeDataSource>, current: RuntimeDataSourceConfig) => {
prev[current.id] = new RuntimeDataSourceItem(current, getRequestHandler(current, requestHandlersMap), context);
return prev;
},
{},
@ -48,6 +38,6 @@ export default (
return {
dataSourceMap,
reloadDataSource: reloadDataSourceFactory(runtimeDataSource, dataSourceMap),
reloadDataSource: reloadDataSourceFactory(runtimeDataSource, dataSourceMap, runtimeDataSource.dataHandler),
};
};

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable no-nested-ternary */
import {
IRuntimeDataSource,
IDataSourceRuntimeContext,
@ -10,17 +9,14 @@ import {
import { RuntimeDataSourceItem } from '../core';
import { reloadDataSourceFactory } from '../core/reloadDataSourceFactory';
import {
defaultDataHandler,
defaultShouldFetch,
defaultWillFetch,
} from '../helpers';
import { defaultDataHandler, defaultShouldFetch, defaultWillFetch, getRequestHandler } from '../helpers';
// TODO: requestConfig mtop 默认的请求 config 怎么处理?
/**
* @param dataSource
* @param context
* @param extraConfig: { requestHandlersMap }
*/
export default (
dataSource: RuntimeDataSource,
context: IDataSourceRuntimeContext,
@ -34,28 +30,19 @@ export default (
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;
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,
// type 协议默认值 fetch
requestHandlersMap[current.type || 'fetch'],
context,
);
(prev: Record<string, IRuntimeDataSource>, current: RuntimeDataSourceConfig) => {
prev[current.id] = new RuntimeDataSourceItem(current, getRequestHandler(current, requestHandlersMap), context);
return prev;
},
{},
@ -63,6 +50,6 @@ export default (
return {
dataSourceMap,
reloadDataSource: reloadDataSourceFactory(dataSource, dataSourceMap),
reloadDataSource: reloadDataSourceFactory(dataSource, dataSourceMap, dataSource.dataHandler),
};
};

View File

@ -0,0 +1,3 @@
# 关于此场景
数据源的 type 可以是 `custom` 类型的, 此时需要提供 `requestHandler` 给数据源

View File

@ -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',
};
},
},
],
};

View File

@ -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',
},
},
],
};

View File

@ -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';

View File

@ -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,
});

View File

@ -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,
});

View File

@ -5,11 +5,17 @@ import {
export type RequestHandler<T = unknown> = (
options: RuntimeOptionsConfig,
context: IDataSourceRuntimeContext
context?: IDataSourceRuntimeContext,
) => Promise<T>;
export type UrlParamsHandler<T = unknown> = (
context?: IDataSourceRuntimeContext
context?: IDataSourceRuntimeContext,
) => Promise<T>;
export type RequestHandlersMap<T = unknown> = Record<string, RequestHandler<T>>;
// 仅在 type=custom 的时候生效的 handler
export type CustomRequestHandler<T = unknown> = (
options: RuntimeOptionsConfig,
context?: IDataSourceRuntimeContext,
) => Promise<T>;

View File

@ -1,10 +1,10 @@
import { IRuntimeDataSource } from './data-source';
import { CustomRequestHandler } from './data-source-handlers';
// 先定义运行模式的类型
export interface RuntimeDataSource {
list: RuntimeDataSourceConfig[];
// TODO: dataMap 格式不对要处理
dataHandler?: (dataMap: DataSourceMap) => void;
dataHandler?: (dataSourceMap: DataSourceMap) => void;
}
export type DataSourceMap = Record<string, IRuntimeDataSource>;
@ -16,7 +16,7 @@ export interface RuntimeDataSourceConfig {
type?: string;
willFetch?: WillFetch;
shouldFetch?: () => boolean;
requestHandler?: () => void; // TODO: 待定
requestHandler?: CustomRequestHandler;
dataHandler?: DataHandler;
errorHandler?: ErrorHandler;
options?: RuntimeOptions;