@ruiapp/rapid-core 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,13 +3,15 @@ export declare const GlobalRequest: {
3
3
  prototype: Request;
4
4
  };
5
5
  export interface RapidRequestBody {
6
- type: string;
6
+ type: "form-data" | "json";
7
7
  value: any;
8
8
  }
9
9
  export declare class RapidRequest {
10
+ #private;
10
11
  method: string;
11
12
  url: URL;
13
+ headers: Headers;
12
14
  hasBody: boolean;
13
- constructor(init: RequestInit);
15
+ constructor(req: Request);
14
16
  body(): RapidRequestBody;
15
17
  }
@@ -12,6 +12,7 @@ export interface IRpdServer {
12
12
  getHttpHandlerByCode(code: string): HttpRequestHandler | undefined;
13
13
  getDataAccessor<T = any>(options: GetDataAccessorOptions): IRpdDataAccessor<T>;
14
14
  getApplicationConfig(): RpdApplicationConfig;
15
+ appendApplicationConfig(config: Partial<RpdApplicationConfig>): any;
15
16
  getModel(options: GetModelOptions): RpdDataModel | undefined;
16
17
  registerEventHandler<K extends keyof RpdServerEventTypes>(eventName: K, listener: (...args: RpdServerEventTypes[K]) => void): this;
17
18
  emitEvent<K extends keyof RpdServerEventTypes>(eventName: K, sender: IPluginInstance, payload: RpdServerEventTypes[K][1]): void;
@@ -1,4 +1,4 @@
1
- import { CountEntityOptions, FindEntityOptions, IRpdDataAccessor, RpdDataModel } from "~/types";
1
+ import { CountEntityOptions, FindEntityOptions, IRpdDataAccessor, RpdDataModel, IDatabaseAccessor } from "~/types";
2
2
  import QueryBuilder from "~/queryBuilder/queryBuilder";
3
3
  export interface IDataAccessorOptions {
4
4
  model: RpdDataModel;
@@ -6,7 +6,7 @@ export interface IDataAccessorOptions {
6
6
  }
7
7
  export default class DataAccessor<T = any> implements IRpdDataAccessor<T> {
8
8
  #private;
9
- constructor(options: IDataAccessorOptions);
9
+ constructor(databaseAccessor: IDatabaseAccessor, options: IDataAccessorOptions);
10
10
  getModel(): RpdDataModel;
11
11
  create(entity: Partial<T>): Promise<T>;
12
12
  updateById(id: any, entity: Partial<T>): Promise<{
package/dist/index.js CHANGED
@@ -41,7 +41,8 @@ class DataAccessor {
41
41
  #model;
42
42
  #queryBuilder;
43
43
  #databaseAccessor;
44
- constructor(options) {
44
+ constructor(databaseAccessor, options) {
45
+ this.#databaseAccessor = databaseAccessor;
45
46
  this.#queryBuilder = options.queryBuilder;
46
47
  this.#model = options.model;
47
48
  }
@@ -153,6 +154,7 @@ class QueryBuilder {
153
154
  }
154
155
  select(model, options) {
155
156
  const ctx = {
157
+ builder: this,
156
158
  params: [],
157
159
  };
158
160
  let { properties, filters, orderBy, pagination } = options;
@@ -191,6 +193,7 @@ class QueryBuilder {
191
193
  }
192
194
  count(model, options) {
193
195
  const ctx = {
196
+ builder: this,
194
197
  params: [],
195
198
  };
196
199
  let { filters } = options;
@@ -208,6 +211,7 @@ class QueryBuilder {
208
211
  insert(model, options) {
209
212
  const params = [];
210
213
  const ctx = {
214
+ builder: this,
211
215
  params,
212
216
  };
213
217
  const { entity } = options;
@@ -242,6 +246,7 @@ class QueryBuilder {
242
246
  update(model, options) {
243
247
  const params = [];
244
248
  const ctx = {
249
+ builder: this,
245
250
  params,
246
251
  };
247
252
  let { entity, filters } = options;
@@ -279,6 +284,7 @@ class QueryBuilder {
279
284
  delete(model, options) {
280
285
  const params = [];
281
286
  const ctx = {
287
+ builder: this,
282
288
  params,
283
289
  };
284
290
  let { filters } = options;
@@ -353,7 +359,7 @@ function buildLogicalFilterQuery(level, ctx, filter) {
353
359
  return command;
354
360
  }
355
361
  function buildUnaryFilterQuery(ctx, filter) {
356
- let command = this.quoteObject(filter.field);
362
+ let command = ctx.builder.quoteObject(filter.field);
357
363
  if (filter.operator === "null") {
358
364
  command += " IS NULL";
359
365
  }
@@ -363,7 +369,7 @@ function buildUnaryFilterQuery(ctx, filter) {
363
369
  return command;
364
370
  }
365
371
  function buildInFilterQuery(ctx, filter) {
366
- let command = this.quoteObject(filter.field);
372
+ let command = ctx.builder.quoteObject(filter.field);
367
373
  if (filter.operator === "in") {
368
374
  command += " = ";
369
375
  }
@@ -375,49 +381,49 @@ function buildInFilterQuery(ctx, filter) {
375
381
  return command;
376
382
  }
377
383
  function buildContainsFilterQuery(ctx, filter) {
378
- let command = this.quoteObject(filter.field);
384
+ let command = ctx.builder.quoteObject(filter.field);
379
385
  command += " LIKE ";
380
386
  ctx.params.push(`%${filter.value}%`);
381
387
  command += "$" + ctx.params.length;
382
388
  return command;
383
389
  }
384
390
  function buildNotContainsFilterQuery(ctx, filter) {
385
- let command = this.quoteObject(filter.field);
391
+ let command = ctx.builder.quoteObject(filter.field);
386
392
  command += " NOT LIKE ";
387
393
  ctx.params.push(`%${filter.value}%`);
388
394
  command += "$" + ctx.params.length;
389
395
  return command;
390
396
  }
391
397
  function buildStartsWithFilterQuery(ctx, filter) {
392
- let command = this.quoteObject(filter.field);
398
+ let command = ctx.builder.quoteObject(filter.field);
393
399
  command += " LIKE ";
394
400
  ctx.params.push(`${filter.value}%`);
395
401
  command += "$" + ctx.params.length;
396
402
  return command;
397
403
  }
398
404
  function buildNotStartsWithFilterQuery(ctx, filter) {
399
- let command = this.quoteObject(filter.field);
405
+ let command = ctx.builder.quoteObject(filter.field);
400
406
  command += " NOT LIKE ";
401
407
  ctx.params.push(`${filter.value}%`);
402
408
  command += "$" + ctx.params.length;
403
409
  return command;
404
410
  }
405
411
  function buildEndsWithFilterQuery(ctx, filter) {
406
- let command = this.quoteObject(filter.field);
412
+ let command = ctx.builder.quoteObject(filter.field);
407
413
  command += " LIKE ";
408
414
  ctx.params.push(`%${filter.value}`);
409
415
  command += "$" + ctx.params.length;
410
416
  return command;
411
417
  }
412
418
  function buildNotEndsWithFilterQuery(ctx, filter) {
413
- let command = this.quoteObject(filter.field);
419
+ let command = ctx.builder.quoteObject(filter.field);
414
420
  command += " NOT LIKE ";
415
421
  ctx.params.push(`%${filter.value}`);
416
422
  command += "$" + ctx.params.length;
417
423
  return command;
418
424
  }
419
425
  function buildRelationalFilterQuery(ctx, filter) {
420
- let command = this.quoteObject(filter.field);
426
+ let command = ctx.builder.quoteObject(filter.field);
421
427
  command += relationalOperatorsMap.get(filter.operator);
422
428
  ctx.params.push(filter.value);
423
429
  command += "$" + ctx.params.length;
@@ -1124,10 +1130,10 @@ async function handleEntityDeleteEvent(server, sender, payload) {
1124
1130
  async function configureModels$3(server, applicationConfig) {
1125
1131
  try {
1126
1132
  const models = await listCollections(server, applicationConfig);
1127
- applicationConfig.models.push(...models);
1133
+ server.appendApplicationConfig({ models });
1128
1134
  }
1129
1135
  catch (ex) {
1130
- console.warn("Failed to loading existing meta of models.", ex.message);
1136
+ console.warn("Failed to loading existing meta of models.", ex);
1131
1137
  }
1132
1138
  }
1133
1139
  function listCollections(server, applicationConfig) {
@@ -1808,7 +1814,7 @@ const routeConfigs = [
1808
1814
  },
1809
1815
  {
1810
1816
  code: "updateById",
1811
- method: "post",
1817
+ method: "patch",
1812
1818
  endpoint: "/:id",
1813
1819
  handlerCode: "updateCollectionEntityById",
1814
1820
  },
@@ -1846,7 +1852,7 @@ async function configureRoutes$3(server, applicationConfig) {
1846
1852
  code: `${namespace}.${singularCode}.${routeConfig.code}`,
1847
1853
  type: "RESTful",
1848
1854
  method: routeConfig.method,
1849
- endpoint: `/api/${namespace}/${pluralCode}${routeConfig.endpoint}`,
1855
+ endpoint: `/${namespace}/${pluralCode}${routeConfig.endpoint}`,
1850
1856
  handlers: [
1851
1857
  {
1852
1858
  code: routeConfig.handlerCode,
@@ -1953,11 +1959,21 @@ var listMetaRoutes = /*#__PURE__*/Object.freeze({
1953
1959
 
1954
1960
  async function buildRoutes(server, applicationConfig) {
1955
1961
  const router = new Router__default["default"]();
1962
+ let baseUrl = server.config.baseUrl;
1963
+ if (baseUrl) {
1964
+ if (baseUrl.endsWith('/')) {
1965
+ baseUrl = baseUrl.substring(0, baseUrl.length - 1);
1966
+ }
1967
+ }
1968
+ else {
1969
+ baseUrl = "";
1970
+ }
1956
1971
  applicationConfig.routes.forEach((routeConfig) => {
1957
1972
  if (routeConfig.type !== "RESTful") {
1958
1973
  return;
1959
1974
  }
1960
- router[routeConfig.method](routeConfig.endpoint, async (routerContext, next) => {
1975
+ const routePath = baseUrl + routeConfig.endpoint;
1976
+ router[routeConfig.method](routePath, async (routerContext, next) => {
1961
1977
  const { request, params } = routerContext;
1962
1978
  let search = request.url.search;
1963
1979
  if (search && search.startsWith("?")) {
@@ -2170,9 +2186,9 @@ async function initPlugin$1(plugin, server) {
2170
2186
  _plugin$1 = plugin;
2171
2187
  }
2172
2188
  async function configureModels$2(server, applicationConfig) {
2173
- for (const model of pluginConfig.models) {
2174
- applicationConfig.models.push(model);
2175
- }
2189
+ server.appendApplicationConfig({
2190
+ models: pluginConfig.models,
2191
+ });
2176
2192
  }
2177
2193
  async function registerEventHandlers$2(server) {
2178
2194
  const events = [
@@ -2631,7 +2647,7 @@ var getMyProfile = {
2631
2647
  code: "",
2632
2648
  type: "RESTful",
2633
2649
  method: "get",
2634
- endpoint: "/api/me",
2650
+ endpoint: "/me",
2635
2651
  handlers: [
2636
2652
  {
2637
2653
  code: "getMyProfile",
@@ -2645,7 +2661,7 @@ var signin = {
2645
2661
  code: "",
2646
2662
  type: "RESTful",
2647
2663
  method: "post",
2648
- endpoint: "/api/signin",
2664
+ endpoint: "/signin",
2649
2665
  handlers: [
2650
2666
  {
2651
2667
  code: "createSession",
@@ -2659,7 +2675,7 @@ var signout = {
2659
2675
  code: "",
2660
2676
  type: "RESTful",
2661
2677
  method: "get",
2662
- endpoint: "/api/signout",
2678
+ endpoint: "/signout",
2663
2679
  handlers: [
2664
2680
  {
2665
2681
  code: "deleteSession",
@@ -2863,8 +2879,8 @@ class RapidResponse {
2863
2879
  headers;
2864
2880
  constructor(body, init) {
2865
2881
  this.body = body;
2866
- this.headers = new Headers(init.headers);
2867
- this.status = init.status;
2882
+ this.headers = new Headers(init?.headers);
2883
+ this.status = init?.status;
2868
2884
  }
2869
2885
  json(obj, status, headers) {
2870
2886
  const body = JSON.stringify(obj);
@@ -2904,7 +2920,11 @@ class RouteContext {
2904
2920
  this.request = request;
2905
2921
  this.state = {};
2906
2922
  this.response = new RapidResponse();
2923
+ // `method` and `path` are used by `koa-tree-router` to match route
2924
+ this.method = request.method;
2925
+ this.path = request.url.pathname;
2907
2926
  }
2927
+ // `koa-tree-router` uses this method to set headers
2908
2928
  set(headerName, headerValue) {
2909
2929
  this.response.headers.set(headerName, headerValue);
2910
2930
  }
@@ -2918,133 +2938,43 @@ class RouteContext {
2918
2938
 
2919
2939
  const GlobalRequest = global.Request;
2920
2940
  class RapidRequest {
2941
+ #raw;
2942
+ #bodyParsed;
2943
+ #body;
2921
2944
  method;
2922
2945
  url;
2946
+ headers;
2923
2947
  hasBody;
2924
- constructor(init) {
2948
+ constructor(req) {
2949
+ this.#raw = req;
2950
+ this.method = req.method;
2951
+ this.url = new URL(req.url);
2952
+ this.headers = req.headers;
2953
+ this.hasBody = false;
2925
2954
  }
2926
2955
  body() {
2927
- return {
2928
- type: "",
2929
- value: {}
2930
- };
2931
- }
2932
- }
2933
-
2934
- class RapidServer {
2935
- #eventManager;
2936
- #middlewares;
2937
- #bootstrapApplicationConfig;
2938
- #applicationConfig;
2939
- #httpHandlersMapByCode;
2940
- #databaseAccessor;
2941
- queryBuilder;
2942
- config;
2943
- databaseConfig;
2944
- #buildedRoutes;
2945
- constructor(options) {
2946
- this.#eventManager = new EventManager();
2947
- this.#middlewares = [];
2948
- this.#bootstrapApplicationConfig = options.applicationConfig;
2949
- this.#applicationConfig = {};
2950
- this.#httpHandlersMapByCode = new Map();
2951
- this.#databaseAccessor = options.databaseAccessor;
2952
- this.queryBuilder = new QueryBuilder({
2953
- dbDefaultSchema: options.databaseConfig.dbDefaultSchema,
2954
- });
2955
- this.databaseConfig = options.databaseConfig;
2956
- this.config = options.serverConfig;
2957
- }
2958
- getApplicationConfig() {
2959
- return this.#applicationConfig;
2960
- }
2961
- registerHttpHandler(plugin, options) {
2962
- const handler = ___namespace.bind(options.handler, null, plugin);
2963
- this.#httpHandlersMapByCode.set(options.code, handler);
2964
- }
2965
- getHttpHandlerByCode(code) {
2966
- return this.#httpHandlersMapByCode.get(code);
2967
- }
2968
- registerMiddleware(middleware) {
2969
- this.#middlewares.push(middleware);
2970
- }
2971
- getDataAccessor(options) {
2972
- const { namespace, singularCode } = options;
2973
- // TODO: Should reuse the and DataAccessor instance
2974
- const model = this.getModel(options);
2975
- if (!model) {
2976
- throw new Error(`Data model ${namespace}.${singularCode} not found.`);
2977
- }
2978
- const dataAccessor = new DataAccessor({
2979
- model,
2980
- queryBuilder: this.queryBuilder,
2981
- });
2982
- return dataAccessor;
2983
- }
2984
- getModel(options) {
2985
- if (options.namespace) {
2986
- return this.#applicationConfig?.models.find((e) => e.namespace === options.namespace && e.singularCode === options.singularCode);
2987
- }
2988
- return this.#applicationConfig?.models.find((e) => e.singularCode === options.singularCode);
2989
- }
2990
- registerEventHandler(eventName, listener) {
2991
- this.#eventManager.on(eventName, listener);
2992
- return this;
2993
- }
2994
- async emitEvent(eventName, sender, payload) {
2995
- console.log(`Emit event "${eventName}"`, payload);
2996
- await this.#eventManager.emit(eventName, sender, payload);
2997
- // TODO: should move this logic into metaManager
2998
- // if (
2999
- // (eventName === "entity.create" || eventName === "entity.update" ||
3000
- // eventName === "entity.delete") &&
3001
- // payload.namespace === "meta"
3002
- // ) {
3003
- // await this.configureApplication();
3004
- // }
3005
- }
3006
- async start() {
3007
- console.log("Starting rapid server...");
3008
- await loadPlugins();
3009
- await initPlugins(this);
3010
- await registerMiddlewares(this);
3011
- await registerHttpHandlers(this);
3012
- await registerEventHandlers(this);
3013
- await registerMessageHandlers(this);
3014
- await registerTaskProcessors(this);
3015
- await this.configureApplication();
3016
- console.log(`Application ready.`);
3017
- await onApplicationReady(this, this.#applicationConfig);
3018
- }
3019
- async configureApplication() {
3020
- this.#applicationConfig = ___namespace.merge({}, this.#bootstrapApplicationConfig);
3021
- await onLoadingApplication(this, this.#applicationConfig);
3022
- await configureModels(this, this.#applicationConfig);
3023
- await configureModelProperties(this, this.#applicationConfig);
3024
- await configureRoutes(this, this.#applicationConfig);
3025
- // TODO: check application configuration.
3026
- await onApplicationLoaded(this, this.#applicationConfig);
3027
- this.#buildedRoutes = await buildRoutes(this, this.#applicationConfig);
3028
- }
3029
- async queryDatabaseObject(sql, params) {
3030
- return await this.#databaseAccessor.queryDatabaseObject(sql, params);
3031
- }
3032
- async tryQueryDatabaseObject(sql, params) {
3033
- try {
3034
- return await this.queryDatabaseObject(sql, params);
3035
- }
3036
- catch (err) {
3037
- console.error(err.message);
2956
+ if (this.#bodyParsed) {
2957
+ return this.#body;
2958
+ }
2959
+ this.#bodyParsed = true;
2960
+ if (this.method === "post" || this.method === "put" || this.method === "patch") {
2961
+ this.hasBody = true;
2962
+ const contentType = this.headers.get("Content-Type");
2963
+ if (contentType.includes("json")) {
2964
+ this.#body = {
2965
+ type: "json",
2966
+ value: this.#raw.json(),
2967
+ };
2968
+ }
2969
+ else if (contentType.includes("application/x-www-form-urlencoded")) {
2970
+ this.#body = {
2971
+ type: "form-data",
2972
+ value: this.#raw.formData(),
2973
+ };
2974
+ }
3038
2975
  }
3039
- return [];
3040
- }
3041
- get middlewares() {
3042
- return this.#middlewares;
3043
- }
3044
- async handleRequest(request, next) {
3045
- const routeContext = new RouteContext(new RapidRequest(request));
3046
- await this.#buildedRoutes(routeContext, next);
3047
- return routeContext.response.getResponse();
2976
+ this.#body = null;
2977
+ return this.#body;
3048
2978
  }
3049
2979
  }
3050
2980
 
@@ -3535,7 +3465,7 @@ var bootstrapApplicationConfig = {
3535
3465
  code: "meta.model.list",
3536
3466
  type: "RESTful",
3537
3467
  method: "get",
3538
- endpoint: "/meta/models",
3468
+ endpoint: "/_meta/models",
3539
3469
  handlers: [
3540
3470
  {
3541
3471
  code: "listMetaModels",
@@ -3548,7 +3478,7 @@ var bootstrapApplicationConfig = {
3548
3478
  code: "meta.model.getDetail",
3549
3479
  type: "RESTful",
3550
3480
  method: "get",
3551
- endpoint: "/meta/models/:namespace/:singularCode",
3481
+ endpoint: "/_meta/models/:namespace/:singularCode",
3552
3482
  handlers: [
3553
3483
  {
3554
3484
  code: "getMetaModelDetail",
@@ -3561,7 +3491,7 @@ var bootstrapApplicationConfig = {
3561
3491
  code: "meta.route.list",
3562
3492
  type: "RESTful",
3563
3493
  method: "get",
3564
- endpoint: "/meta/routes",
3494
+ endpoint: "/_meta/routes",
3565
3495
  handlers: [
3566
3496
  {
3567
3497
  code: "listMetaRoutes",
@@ -3576,6 +3506,160 @@ var bootstrapApplicationConfig$1 = /*#__PURE__*/Object.freeze({
3576
3506
  'default': bootstrapApplicationConfig
3577
3507
  });
3578
3508
 
3509
+ class RapidServer {
3510
+ #eventManager;
3511
+ #middlewares;
3512
+ #bootstrapApplicationConfig;
3513
+ #applicationConfig;
3514
+ #httpHandlersMapByCode;
3515
+ #databaseAccessor;
3516
+ queryBuilder;
3517
+ config;
3518
+ databaseConfig;
3519
+ #buildedRoutes;
3520
+ constructor(options) {
3521
+ this.#eventManager = new EventManager();
3522
+ this.#middlewares = [];
3523
+ this.#bootstrapApplicationConfig = options.applicationConfig || bootstrapApplicationConfig;
3524
+ this.#applicationConfig = {};
3525
+ this.#httpHandlersMapByCode = new Map();
3526
+ this.#databaseAccessor = options.databaseAccessor;
3527
+ this.queryBuilder = new QueryBuilder({
3528
+ dbDefaultSchema: options.databaseConfig.dbDefaultSchema,
3529
+ });
3530
+ this.databaseConfig = options.databaseConfig;
3531
+ this.config = options.serverConfig;
3532
+ }
3533
+ getApplicationConfig() {
3534
+ return this.#applicationConfig;
3535
+ }
3536
+ appendApplicationConfig(config) {
3537
+ const { models, routes } = config;
3538
+ if (models) {
3539
+ for (const model of models) {
3540
+ const originalModel = ___namespace.find(this.#applicationConfig.models, (item) => item.singularCode == model.singularCode);
3541
+ if (originalModel) {
3542
+ originalModel.name = model.name;
3543
+ const originalProperties = originalModel.properties;
3544
+ for (const property of model.properties) {
3545
+ const originalProperty = ___namespace.find(originalProperties, (item) => item.code == property.code);
3546
+ if (originalProperty) {
3547
+ originalProperty.name = property.name;
3548
+ }
3549
+ else {
3550
+ originalProperties.push(property);
3551
+ }
3552
+ }
3553
+ }
3554
+ else {
3555
+ this.#applicationConfig.models.push(model);
3556
+ }
3557
+ }
3558
+ }
3559
+ if (routes) {
3560
+ for (const route of routes) {
3561
+ const originalRoute = ___namespace.find(this.#applicationConfig.routes, (item) => item.code == route.code);
3562
+ if (originalRoute) {
3563
+ originalRoute.name = route.name;
3564
+ originalRoute.handlers = route.handlers;
3565
+ }
3566
+ else {
3567
+ this.#applicationConfig.routes.push(route);
3568
+ }
3569
+ }
3570
+ }
3571
+ }
3572
+ registerHttpHandler(plugin, options) {
3573
+ const handler = ___namespace.bind(options.handler, null, plugin);
3574
+ this.#httpHandlersMapByCode.set(options.code, handler);
3575
+ }
3576
+ getHttpHandlerByCode(code) {
3577
+ return this.#httpHandlersMapByCode.get(code);
3578
+ }
3579
+ registerMiddleware(middleware) {
3580
+ this.#middlewares.push(middleware);
3581
+ }
3582
+ getDataAccessor(options) {
3583
+ const { namespace, singularCode } = options;
3584
+ // TODO: Should reuse the and DataAccessor instance
3585
+ const model = this.getModel(options);
3586
+ if (!model) {
3587
+ throw new Error(`Data model ${namespace}.${singularCode} not found.`);
3588
+ }
3589
+ const dataAccessor = new DataAccessor(this.#databaseAccessor, {
3590
+ model,
3591
+ queryBuilder: this.queryBuilder,
3592
+ });
3593
+ return dataAccessor;
3594
+ }
3595
+ getModel(options) {
3596
+ if (options.namespace) {
3597
+ return this.#applicationConfig?.models.find((e) => e.namespace === options.namespace && e.singularCode === options.singularCode);
3598
+ }
3599
+ return this.#applicationConfig?.models.find((e) => e.singularCode === options.singularCode);
3600
+ }
3601
+ registerEventHandler(eventName, listener) {
3602
+ this.#eventManager.on(eventName, listener);
3603
+ return this;
3604
+ }
3605
+ async emitEvent(eventName, sender, payload) {
3606
+ console.log(`Emit event "${eventName}"`, payload);
3607
+ await this.#eventManager.emit(eventName, sender, payload);
3608
+ // TODO: should move this logic into metaManager
3609
+ // if (
3610
+ // (eventName === "entity.create" || eventName === "entity.update" ||
3611
+ // eventName === "entity.delete") &&
3612
+ // payload.namespace === "meta"
3613
+ // ) {
3614
+ // await this.configureApplication();
3615
+ // }
3616
+ }
3617
+ async start() {
3618
+ console.log("Starting rapid server...");
3619
+ await loadPlugins();
3620
+ await initPlugins(this);
3621
+ await registerMiddlewares(this);
3622
+ await registerHttpHandlers(this);
3623
+ await registerEventHandlers(this);
3624
+ await registerMessageHandlers(this);
3625
+ await registerTaskProcessors(this);
3626
+ await this.configureApplication();
3627
+ console.log(`Application ready.`);
3628
+ await onApplicationReady(this, this.#applicationConfig);
3629
+ }
3630
+ async configureApplication() {
3631
+ this.#applicationConfig = ___namespace.merge({}, this.#bootstrapApplicationConfig);
3632
+ await onLoadingApplication(this, this.#applicationConfig);
3633
+ await configureModels(this, this.#applicationConfig);
3634
+ await configureModelProperties(this, this.#applicationConfig);
3635
+ await configureRoutes(this, this.#applicationConfig);
3636
+ // TODO: check application configuration.
3637
+ await onApplicationLoaded(this, this.#applicationConfig);
3638
+ this.#buildedRoutes = await buildRoutes(this, this.#applicationConfig);
3639
+ }
3640
+ async queryDatabaseObject(sql, params) {
3641
+ return await this.#databaseAccessor.queryDatabaseObject(sql, params);
3642
+ }
3643
+ async tryQueryDatabaseObject(sql, params) {
3644
+ try {
3645
+ return await this.queryDatabaseObject(sql, params);
3646
+ }
3647
+ catch (err) {
3648
+ console.error(err.message);
3649
+ }
3650
+ return [];
3651
+ }
3652
+ get middlewares() {
3653
+ return this.#middlewares;
3654
+ }
3655
+ async handleRequest(request, next) {
3656
+ const routeContext = new RouteContext(new RapidRequest(request));
3657
+ console.log('routeContext', routeContext);
3658
+ await this.#buildedRoutes(routeContext, next);
3659
+ return routeContext.response.getResponse();
3660
+ }
3661
+ }
3662
+
3579
3663
  fixBigIntJSONSerialize();
3580
3664
 
3581
3665
  exports.GlobalRequest = GlobalRequest;
@@ -1,5 +1,6 @@
1
1
  import { CountEntityOptions, DeleteEntityOptions, EntityFilterOptions, FindEntityOptions, CreateEntityOptions, RpdDataModel, UpdateEntityOptions, QuoteTableOptions } from "../types";
2
2
  export interface BuildQueryContext {
3
+ builder: QueryBuilder;
3
4
  params: any[];
4
5
  }
5
6
  export interface InitQueryBuilderOptions {
package/dist/server.d.ts CHANGED
@@ -6,7 +6,7 @@ export interface InitServerOptions {
6
6
  databaseAccessor: IDatabaseAccessor;
7
7
  databaseConfig: IDatabaseConfig;
8
8
  serverConfig: RapidServerConfig;
9
- applicationConfig: RpdApplicationConfig;
9
+ applicationConfig?: RpdApplicationConfig;
10
10
  }
11
11
  export declare class RapidServer implements IRpdServer {
12
12
  #private;
@@ -15,6 +15,7 @@ export declare class RapidServer implements IRpdServer {
15
15
  databaseConfig: IDatabaseConfig;
16
16
  constructor(options: InitServerOptions);
17
17
  getApplicationConfig(): RpdApplicationConfig;
18
+ appendApplicationConfig(config: Partial<RpdApplicationConfig>): void;
18
19
  registerHttpHandler(plugin: IPluginInstance, options: IPluginHttpHandler): void;
19
20
  getHttpHandlerByCode(code: string): HttpRequestHandler;
20
21
  registerMiddleware(middleware: any): void;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type RapidServerConfig = {
2
+ baseUrl?: string;
2
3
  sessionCookieName: string;
3
4
  jwtKey: string;
4
5
  localFileStoragePath: string;
@@ -246,7 +247,7 @@ export interface RpdRoute {
246
247
  endpoint: string;
247
248
  handlers: RpdHttpHandler[];
248
249
  }
249
- export type RpdHttpMethod = "get" | "post" | "put" | "delete";
250
+ export type RpdHttpMethod = "get" | "post" | "put" | "delete" | "patch";
250
251
  export interface RpdHttpHandler {
251
252
  code: string;
252
253
  config: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "keywords": [],
@@ -487,7 +487,7 @@ export default {
487
487
  code: "meta.model.list",
488
488
  type: "RESTful",
489
489
  method: "get",
490
- endpoint: "/meta/models",
490
+ endpoint: "/_meta/models",
491
491
  handlers: [
492
492
  {
493
493
  code: "listMetaModels",
@@ -500,7 +500,7 @@ export default {
500
500
  code: "meta.model.getDetail",
501
501
  type: "RESTful",
502
502
  method: "get",
503
- endpoint: "/meta/models/:namespace/:singularCode",
503
+ endpoint: "/_meta/models/:namespace/:singularCode",
504
504
  handlers: [
505
505
  {
506
506
  code: "getMetaModelDetail",
@@ -513,7 +513,7 @@ export default {
513
513
  code: "meta.route.list",
514
514
  type: "RESTful",
515
515
  method: "get",
516
- endpoint: "/meta/routes",
516
+ endpoint: "/_meta/routes",
517
517
  handlers: [
518
518
  {
519
519
  code: "listMetaRoutes",
@@ -1,23 +1,54 @@
1
1
  export const GlobalRequest = global.Request;
2
2
 
3
3
 
4
+
4
5
  export interface RapidRequestBody {
5
- type: string;
6
+ type: "form-data" | "json";
6
7
  value: any;
7
8
  }
8
9
 
9
10
  export class RapidRequest {
11
+ #raw: Request;
12
+ #bodyParsed: boolean;
13
+ #body: RapidRequestBody;
10
14
  method: string;
11
15
  url: URL;
16
+ headers: Headers;
12
17
  hasBody: boolean;
13
18
 
14
- constructor(init: RequestInit) {
19
+ constructor(req: Request) {
20
+ this.#raw = req;
21
+ this.method = req.method;
22
+ this.url = new URL(req.url);
23
+ this.headers = req.headers;
24
+ this.hasBody = false;
15
25
  }
16
26
 
17
27
  body(): RapidRequestBody {
18
- return {
19
- type: "",
20
- value: {}
28
+ if (this.#bodyParsed) {
29
+ return this.#body;
30
+ }
31
+
32
+ this.#bodyParsed = true;
33
+
34
+ if (this.method === "post" || this.method === "put" || this.method === "patch") {
35
+ this.hasBody = true;
36
+
37
+ const contentType = this.headers.get("Content-Type");
38
+ if (contentType.includes("json")) {
39
+ this.#body = {
40
+ type: "json",
41
+ value: this.#raw.json(),
42
+ };
43
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
44
+ this.#body = {
45
+ type: "form-data",
46
+ value: this.#raw.formData(),
47
+ }
48
+ }
21
49
  }
50
+
51
+ this.#body = null;
52
+ return this.#body;
22
53
  }
23
54
  }
@@ -40,8 +40,8 @@ export class RapidResponse {
40
40
 
41
41
  constructor(body?: BodyInit, init?: ResponseInit) {
42
42
  this.body = body;
43
- this.headers = new Headers(init.headers);
44
- this.status = init.status;
43
+ this.headers = new Headers(init?.headers);
44
+ this.status = init?.status;
45
45
  }
46
46
 
47
47
  json(
@@ -18,8 +18,13 @@ export class RouteContext {
18
18
  this.request = request;
19
19
  this.state = {};
20
20
  this.response = new RapidResponse();
21
+
22
+ // `method` and `path` are used by `koa-tree-router` to match route
23
+ this.method = request.method;
24
+ this.path = request.url.pathname;
21
25
  }
22
26
 
27
+ // `koa-tree-router` uses this method to set headers
23
28
  set(headerName: string, headerValue: string) {
24
29
  this.response.headers.set(headerName, headerValue);
25
30
  }
@@ -12,13 +12,24 @@ export async function buildRoutes(
12
12
  ) {
13
13
  const router = new Router();
14
14
 
15
+ let baseUrl = server.config.baseUrl;
16
+ if (baseUrl) {
17
+ if (baseUrl.endsWith('/')) {
18
+ baseUrl = baseUrl.substring(0, baseUrl.length - 1);
19
+ }
20
+ } else {
21
+ baseUrl = "";
22
+ }
23
+
15
24
  applicationConfig.routes.forEach((routeConfig) => {
16
25
  if (routeConfig.type !== "RESTful") {
17
26
  return;
18
27
  }
19
28
 
29
+ const routePath = baseUrl + routeConfig.endpoint;
30
+
20
31
  (router as any)[routeConfig.method](
21
- routeConfig.endpoint,
32
+ routePath,
22
33
  async (routerContext: RouteContext, next: Next) => {
23
34
  const { request, params } = routerContext;
24
35
 
@@ -24,6 +24,7 @@ export interface IRpdServer {
24
24
  options: GetDataAccessorOptions,
25
25
  ): IRpdDataAccessor<T>;
26
26
  getApplicationConfig(): RpdApplicationConfig;
27
+ appendApplicationConfig(config: Partial<RpdApplicationConfig>);
27
28
  getModel(options: GetModelOptions): RpdDataModel | undefined;
28
29
  registerEventHandler<K extends keyof RpdServerEventTypes>(
29
30
  eventName: K,
@@ -20,7 +20,8 @@ export default class DataAccessor<T = any> implements IRpdDataAccessor<T> {
20
20
  #queryBuilder: QueryBuilder;
21
21
  #databaseAccessor: IDatabaseAccessor;
22
22
 
23
- constructor(options: IDataAccessorOptions) {
23
+ constructor(databaseAccessor: IDatabaseAccessor, options: IDataAccessorOptions) {
24
+ this.#databaseAccessor = databaseAccessor;
24
25
  this.#queryBuilder = options.queryBuilder;
25
26
  this.#model = options.model;
26
27
  }
@@ -6,7 +6,7 @@ export default {
6
6
  code: "",
7
7
  type: "RESTful",
8
8
  method: "get",
9
- endpoint: "/api/me",
9
+ endpoint: "/me",
10
10
  handlers: [
11
11
  {
12
12
  code: "getMyProfile",
@@ -6,7 +6,7 @@ export default {
6
6
  code: "",
7
7
  type: "RESTful",
8
8
  method: "post",
9
- endpoint: "/api/signin",
9
+ endpoint: "/signin",
10
10
  handlers: [
11
11
  {
12
12
  code: "createSession",
@@ -6,7 +6,7 @@ export default {
6
6
  code: "",
7
7
  type: "RESTful",
8
8
  method: "get",
9
- endpoint: "/api/signout",
9
+ endpoint: "/signout",
10
10
  handlers: [
11
11
  {
12
12
  code: "deleteSession",
@@ -80,7 +80,7 @@ const routeConfigs: {
80
80
  },
81
81
  {
82
82
  code: "updateById",
83
- method: "post",
83
+ method: "patch",
84
84
  endpoint: "/:id",
85
85
  handlerCode: "updateCollectionEntityById",
86
86
  },
@@ -127,7 +127,7 @@ export async function configureRoutes(
127
127
  code: `${namespace}.${singularCode}.${routeConfig.code}`,
128
128
  type: "RESTful",
129
129
  method: routeConfig.method,
130
- endpoint: `/api/${namespace}/${pluralCode}${routeConfig.endpoint}`,
130
+ endpoint: `/${namespace}/${pluralCode}${routeConfig.endpoint}`,
131
131
  handlers: [
132
132
  {
133
133
  code: routeConfig.handlerCode,
@@ -155,10 +155,10 @@ export async function configureModels(
155
155
  applicationConfig: RpdApplicationConfig,
156
156
  ) {
157
157
  try {
158
- const models = await listCollections(server, applicationConfig);
159
- applicationConfig.models.push(...models);
158
+ const models: RpdDataModel[] = await listCollections(server, applicationConfig);
159
+ server.appendApplicationConfig({ models });
160
160
  } catch (ex) {
161
- console.warn("Failed to loading existing meta of models.", ex.message);
161
+ console.warn("Failed to loading existing meta of models.", ex);
162
162
  }
163
163
  }
164
164
 
@@ -44,9 +44,9 @@ export async function configureModels(
44
44
  server: IRpdServer,
45
45
  applicationConfig: RpdApplicationConfig,
46
46
  ) {
47
- for (const model of pluginConfig.models) {
48
- applicationConfig.models.push(model);
49
- }
47
+ server.appendApplicationConfig({
48
+ models: pluginConfig.models,
49
+ });
50
50
  }
51
51
 
52
52
  export async function registerEventHandlers(server: IRpdServer) {
@@ -32,6 +32,7 @@ const relationalOperatorsMap = new Map<EntityFilterRelationalOperators, string>(
32
32
 
33
33
 
34
34
  export interface BuildQueryContext {
35
+ builder: QueryBuilder;
35
36
  params: any[];
36
37
  }
37
38
 
@@ -63,6 +64,7 @@ export default class QueryBuilder {
63
64
 
64
65
  select(model: RpdDataModel, options: FindEntityOptions) {
65
66
  const ctx: BuildQueryContext = {
67
+ builder: this,
66
68
  params: [],
67
69
  };
68
70
  let { properties, filters, orderBy, pagination } = options;
@@ -107,6 +109,7 @@ export default class QueryBuilder {
107
109
 
108
110
  count(model: RpdDataModel, options: CountEntityOptions) {
109
111
  const ctx: BuildQueryContext = {
112
+ builder: this,
110
113
  params: [],
111
114
  };
112
115
  let { filters } = options;
@@ -128,6 +131,7 @@ export default class QueryBuilder {
128
131
  insert(model: RpdDataModel, options: CreateEntityOptions) {
129
132
  const params: any[] = [];
130
133
  const ctx: BuildQueryContext = {
134
+ builder: this,
131
135
  params,
132
136
  };
133
137
  const { entity } = options;
@@ -171,6 +175,7 @@ export default class QueryBuilder {
171
175
  update(model: RpdDataModel, options: UpdateEntityOptions) {
172
176
  const params: any[] = [];
173
177
  const ctx: BuildQueryContext = {
178
+ builder: this,
174
179
  params,
175
180
  };
176
181
  let { entity, filters } = options;
@@ -218,6 +223,7 @@ export default class QueryBuilder {
218
223
  delete(model: RpdDataModel, options: DeleteEntityOptions) {
219
224
  const params: any[] = [];
220
225
  const ctx: BuildQueryContext = {
226
+ builder: this,
221
227
  params,
222
228
  };
223
229
  let { filters } = options;
@@ -305,7 +311,7 @@ function buildUnaryFilterQuery(
305
311
  ctx: BuildQueryContext,
306
312
  filter: FindEntityUnaryFilterOptions,
307
313
  ) {
308
- let command = this.quoteObject(filter.field);
314
+ let command = ctx.builder.quoteObject(filter.field);
309
315
  if (filter.operator === "null") {
310
316
  command += " IS NULL";
311
317
  } else {
@@ -318,7 +324,7 @@ function buildInFilterQuery(
318
324
  ctx: BuildQueryContext,
319
325
  filter: FindEntitySetFilterOptions,
320
326
  ) {
321
- let command = this.quoteObject(filter.field);
327
+ let command = ctx.builder.quoteObject(filter.field);
322
328
 
323
329
  if (filter.operator === "in") {
324
330
  command += " = ";
@@ -335,7 +341,7 @@ function buildContainsFilterQuery(
335
341
  ctx: BuildQueryContext,
336
342
  filter: FindEntityRelationalFilterOptions,
337
343
  ) {
338
- let command = this.quoteObject(filter.field);
344
+ let command = ctx.builder.quoteObject(filter.field);
339
345
 
340
346
  command += " LIKE ";
341
347
  ctx.params.push(`%${filter.value}%`);
@@ -348,7 +354,7 @@ function buildNotContainsFilterQuery(
348
354
  ctx: BuildQueryContext,
349
355
  filter: FindEntityRelationalFilterOptions,
350
356
  ) {
351
- let command = this.quoteObject(filter.field);
357
+ let command = ctx.builder.quoteObject(filter.field);
352
358
 
353
359
  command += " NOT LIKE ";
354
360
  ctx.params.push(`%${filter.value}%`);
@@ -361,7 +367,7 @@ function buildStartsWithFilterQuery(
361
367
  ctx: BuildQueryContext,
362
368
  filter: FindEntityRelationalFilterOptions,
363
369
  ) {
364
- let command = this.quoteObject(filter.field);
370
+ let command = ctx.builder.quoteObject(filter.field);
365
371
 
366
372
  command += " LIKE ";
367
373
  ctx.params.push(`${filter.value}%`);
@@ -374,7 +380,7 @@ function buildNotStartsWithFilterQuery(
374
380
  ctx: BuildQueryContext,
375
381
  filter: FindEntityRelationalFilterOptions,
376
382
  ) {
377
- let command = this.quoteObject(filter.field);
383
+ let command = ctx.builder.quoteObject(filter.field);
378
384
 
379
385
  command += " NOT LIKE ";
380
386
  ctx.params.push(`${filter.value}%`);
@@ -387,7 +393,7 @@ function buildEndsWithFilterQuery(
387
393
  ctx: BuildQueryContext,
388
394
  filter: FindEntityRelationalFilterOptions,
389
395
  ) {
390
- let command = this.quoteObject(filter.field);
396
+ let command = ctx.builder.quoteObject(filter.field);
391
397
 
392
398
  command += " LIKE ";
393
399
  ctx.params.push(`%${filter.value}`);
@@ -400,7 +406,7 @@ function buildNotEndsWithFilterQuery(
400
406
  ctx: BuildQueryContext,
401
407
  filter: FindEntityRelationalFilterOptions,
402
408
  ) {
403
- let command = this.quoteObject(filter.field);
409
+ let command = ctx.builder.quoteObject(filter.field);
404
410
 
405
411
  command += " NOT LIKE ";
406
412
  ctx.params.push(`%${filter.value}`);
@@ -413,7 +419,7 @@ function buildRelationalFilterQuery(
413
419
  ctx: BuildQueryContext,
414
420
  filter: FindEntityRelationalFilterOptions,
415
421
  ) {
416
- let command = this.quoteObject(filter.field);
422
+ let command = ctx.builder.quoteObject(filter.field);
417
423
 
418
424
  command += relationalOperatorsMap.get(filter.operator);
419
425
 
package/src/server.ts CHANGED
@@ -63,6 +63,41 @@ export class RapidServer implements IRpdServer {
63
63
  return this.#applicationConfig;
64
64
  }
65
65
 
66
+ appendApplicationConfig(config: Partial<RpdApplicationConfig>) {
67
+ const { models, routes } = config;
68
+ if (models) {
69
+ for (const model of models) {
70
+ const originalModel = _.find(this.#applicationConfig.models, (item) => item.singularCode == model.singularCode);
71
+ if (originalModel) {
72
+ originalModel.name = model.name;
73
+ const originalProperties = originalModel.properties;
74
+ for (const property of model.properties) {
75
+ const originalProperty = _.find(originalProperties, (item) => item.code == property.code);
76
+ if (originalProperty) {
77
+ originalProperty.name = property.name;
78
+ } else {
79
+ originalProperties.push(property);
80
+ }
81
+ }
82
+ } else {
83
+ this.#applicationConfig.models.push(model);
84
+ }
85
+ }
86
+ }
87
+
88
+ if (routes) {
89
+ for (const route of routes) {
90
+ const originalRoute = _.find(this.#applicationConfig.routes, (item) => item.code == route.code);
91
+ if (originalRoute) {
92
+ originalRoute.name = route.name;
93
+ originalRoute.handlers = route.handlers;
94
+ } else {
95
+ this.#applicationConfig.routes.push(route);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
66
101
  registerHttpHandler(
67
102
  plugin: IPluginInstance,
68
103
  options: IPluginHttpHandler,
@@ -89,7 +124,7 @@ export class RapidServer implements IRpdServer {
89
124
  throw new Error(`Data model ${namespace}.${singularCode} not found.`);
90
125
  }
91
126
 
92
- const dataAccessor = new DataAccessor<T>({
127
+ const dataAccessor = new DataAccessor<T>(this.#databaseAccessor, {
93
128
  model,
94
129
  queryBuilder: this.queryBuilder as QueryBuilder,
95
130
  });
@@ -186,6 +221,7 @@ export class RapidServer implements IRpdServer {
186
221
 
187
222
  async handleRequest(request: Request, next: Next) {
188
223
  const routeContext = new RouteContext(new RapidRequest(request));
224
+ console.log('routeContext', routeContext)
189
225
  await this.#buildedRoutes(routeContext, next);
190
226
  return routeContext.response.getResponse();
191
227
  }
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type RapidServerConfig = {
2
+ baseUrl?: string;
2
3
  sessionCookieName: string;
3
4
  jwtKey: string;
4
5
  localFileStoragePath: string;
@@ -303,7 +304,7 @@ export interface RpdRoute {
303
304
  handlers: RpdHttpHandler[];
304
305
  }
305
306
 
306
- export type RpdHttpMethod = "get" | "post" | "put" | "delete";
307
+ export type RpdHttpMethod = "get" | "post" | "put" | "delete" | "patch";
307
308
 
308
309
  export interface RpdHttpHandler {
309
310
  code: string;