/
resourcer.ts
306 lines (267 loc) · 7.26 KB
/
resourcer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
import qs from 'qs';
import glob from 'glob';
import compose from 'koa-compose';
import Action, { ActionName } from './action';
import Resource, { ResourceOptions } from './resource';
import { parseRequest, getNameByParams, ParsedParams, requireModule } from './utils';
import { pathToRegexp } from 'path-to-regexp';
export interface ResourcerContext {
resourcer?: Resourcer;
action?: Action;
params?: ParsedParams;
[key: string]: any;
}
export interface KoaMiddlewareOptions {
/**
* 前缀
*/
prefix?: string;
/**
* 自定义 resource name 的获取规则
*
* 默认规则 relatedTable ? relatedTable.table : table
*/
nameRule?: (params: ParsedParams) => string;
/**
* 上下文中的 key - ctx[paramsKey]
*
* 可以单独配置 paramsKey,默认为 params
*/
paramsKey?: string;
/**
* 自定义 action name
*
* 默认为
*
* - list 查看列表
* - create 新增数据
* - get 查看数据详情
* - update 更新数据
* - delete 删除数据
*/
accessors?: {
/**
* 查看列表
*/
list?: string;
/**
* 新增数据
*/
create?: string;
/**
* 查看数据详情
*/
get?: string;
/**
* 更新数据
*/
update?: string;
/**
* 删除数据
*/
delete?: string;
};
}
export interface ExecuteOptions {
/**
* 资源名称
*/
resource: string;
/**
* 自定义 action name
*
* 默认
* - list 查看列表
* - create 新增数据
* - get 查看数据详情
* - update 更新数据
* - delete 删除数据
*/
action: ActionName;
}
export type HandlerType = (ctx: ResourcerContext, next: () => Promise<any>) => any;
export interface Handlers {
[key: string]: HandlerType;
}
export interface ImportOptions {
/**
* 指定配置所在路径
*/
directory: string;
/**
* 文件后缀,默认值 ['js', 'ts', 'json']
*/
extensions?: string[];
}
export class Resourcer {
protected resources = new Map<string, Resource>();
/**
* 全局定义的 action handlers
*/
protected handlers = new Map<ActionName, any>();
protected paramsKey = 'params';
protected middlewares = [];
/**
* 载入指定目录下的 resource 配置(配置的文件驱动)
*
* TODO: 配置的文件驱动现在会全部初始化,大数据时可能存在性能瓶颈,后续可以加入动态加载
*
* @param {object} [options]
* @param {string} [options.directory] 指定配置所在路径
* @param {array} [options.extensions = ['js', 'ts', 'json']] 文件后缀
*/
public import(options: ImportOptions): Map<string, Resource> {
const { extensions = ['js', 'ts', 'json'], directory } = options;
const patten = `${directory}/*.{${extensions.join(',')}}`;
const files = glob.sync(patten, {
ignore: [
'**/*.d.ts'
]
});
const resources = new Map<string, Resource>();
files.forEach((file: string) => {
const options = requireModule(file);
const table = this.define(typeof options === 'function' ? options(this) : options);
resources.set(table.getName(), table);
});
return resources;
}
/**
* resource 配置
*
* @param name
* @param options
*/
define(options: ResourceOptions) {
const { name } = options;
const resource = new Resource(options, this);
this.resources.set(name, resource);
return resource;
}
isDefined(name: string) {
return this.resources.has(name);
}
/**
* 注册全局的 action handlers
*
* @param handlers
*/
registerHandlers(handlers: Handlers) {
this.handlers = new Map(Object.entries(handlers));
}
registerHandler(name: ActionName, handler: HandlerType) {
this.handlers.set(name, handler);
}
getRegisteredHandler(name: ActionName) {
return this.handlers.get(name);
}
getRegisteredHandlers() {
return this.handlers;
}
getResource(name: string): Resource {
if (!this.resources.has(name)) {
throw new Error(`${name} resource does not exist`);
}
return this.resources.get(name);
}
getAction(name: string, action: ActionName): Action {
// 支持注册局部 action
if (this.handlers.has(`${name}.${action}`)) {
return this.getResource(name).getAction(`${name}:${action}`);
}
return this.getResource(name).getAction(action);
}
getParamsKey() {
return this.paramsKey;
}
getMiddlewares() {
return this.middlewares;
}
use(middlewares: HandlerType | HandlerType[]) {
if (typeof middlewares === 'function') {
this.middlewares.push(middlewares);
} else if (Array.isArray(middlewares)) {
this.middlewares.push(...middlewares);
}
}
middleware(options: KoaMiddlewareOptions = {}) {
const { prefix, accessors, paramsKey = 'params', nameRule = getNameByParams } = options;
return async (ctx: ResourcerContext, next: () => Promise<any>) => {
ctx.resourcer = this;
let params = parseRequest({
path: ctx.request.path,
method: ctx.request.method,
}, {
prefix,
accessors,
});
if (!params) {
return next();
}
try {
const resource = this.getResource(nameRule(params));
// 为关系资源时,暂时需要再执行一遍 parseRequest
if (resource.options.type !== 'single') {
params = parseRequest({
path: ctx.request.path,
method: ctx.request.method,
type: resource.options.type,
}, {
prefix,
accessors,
});
if (!params) {
return next();
}
}
// action 需要 clone 之后再赋给 ctx
ctx.action = this.getAction(nameRule(params), params.actionName).clone();
ctx.action.setContext(ctx);
// 自带 query 处理的不太给力,需要用 qs 转一下
const query = qs.parse(qs.stringify(ctx.query));
// filter 支持 json string
if (typeof query.filter === 'string') {
query.filter = JSON.parse(query.filter);
}
// 兼容 ctx.params 的处理,之后的版本里会去掉
ctx[paramsKey] = {
table: params.resourceName,
tableKey: params.resourceKey,
relatedTable: params.associatedName,
relatedKey: params.resourceKey,
action: params.actionName,
};
if (pathToRegexp('/resourcer/{:associatedName.}?:resourceName{\\::actionName}').test(ctx.request.path)) {
await ctx.action.mergeParams({
...query,
...params,
...ctx.request.body,
});
} else {
await ctx.action.mergeParams({
...query,
...params,
values: ctx.request.body,
});
}
return compose(ctx.action.getHandlers())(ctx, next);
} catch (error) {
return next();
}
}
}
/**
* 实验性 API
*
* @param options
* @param context
* @param next
*/
async execute(options: ExecuteOptions, context: ResourcerContext = {}, next?: any) {
const { resource, action } = options;
context.resourcer = this;
context.action = this.getAction(resource, action);
return await context.action.execute(context, next);
}
}
export default Resourcer;