@ruiapp/rapid-core 0.5.0 → 0.5.2

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.
package/dist/index.js CHANGED
@@ -202,8 +202,8 @@ const pgPropertyTypeColumnMap = {
202
202
  "image[]": "jsonb",
203
203
  };
204
204
 
205
- const objLeftQuoteChar = "\"";
206
- const objRightQuoteChar = "\"";
205
+ const objLeftQuoteChar = '"';
206
+ const objRightQuoteChar = '"';
207
207
  const relationalOperatorsMap = new Map([
208
208
  ["eq", "="],
209
209
  ["ne", "<>"],
@@ -250,6 +250,9 @@ class QueryBuilder {
250
250
  }
251
251
  }
252
252
  }
253
+ formatValueToSqlLiteral(value) {
254
+ return formatValueToSqlLiteral(value);
255
+ }
253
256
  select(model, options) {
254
257
  const ctx = {
255
258
  model,
@@ -369,7 +372,7 @@ class QueryBuilder {
369
372
  paramToLiteral: false,
370
373
  };
371
374
  let { filters } = options;
372
- let command = "SELECT COUNT(*)::int as \"count\" FROM ";
375
+ let command = 'SELECT COUNT(*)::int as "count" FROM ';
373
376
  command += this.quoteTable(model);
374
377
  if (filters && filters.length) {
375
378
  command += " WHERE ";
@@ -389,7 +392,7 @@ class QueryBuilder {
389
392
  paramToLiteral: false,
390
393
  };
391
394
  let { filters } = options;
392
- let command = "SELECT COUNT(*)::int as \"count\" FROM ";
395
+ let command = 'SELECT COUNT(*)::int as "count" FROM ';
393
396
  command += `${this.quoteTable(derivedModel)} LEFT JOIN ${this.quoteTable(baseModel)} ON ${this.quoteObject(derivedModel.tableName)}.id = ${this.quoteObject(baseModel.tableName)}.id`;
394
397
  if (filters && filters.length) {
395
398
  command += " WHERE ";
@@ -996,7 +999,6 @@ function mergeHeaders(target, source) {
996
999
  else if (lodash.isObject(source)) {
997
1000
  Object.entries(source).forEach(([key, value]) => target.set(key, value));
998
1001
  }
999
- return target;
1000
1002
  }
1001
1003
  function newResponse(options) {
1002
1004
  return new Response(options.body, {
@@ -1005,6 +1007,7 @@ function newResponse(options) {
1005
1007
  });
1006
1008
  }
1007
1009
  class RapidResponse {
1010
+ // TODO: remove this field.
1008
1011
  #response;
1009
1012
  status;
1010
1013
  body;
@@ -1024,13 +1027,16 @@ class RapidResponse {
1024
1027
  if (headers) {
1025
1028
  mergeHeaders(responseHeaders, headers);
1026
1029
  }
1027
- this.#response = newResponse({ body, status: status || 200, headers: responseHeaders });
1030
+ this.status = status || 200;
1031
+ this.body = body;
1032
+ this.#response = newResponse({ body, status: this.status, headers: responseHeaders });
1028
1033
  }
1029
1034
  redirect(location, status) {
1030
1035
  this.headers.set("Location", location);
1036
+ this.status = status || 302;
1031
1037
  this.#response = newResponse({
1032
1038
  headers: this.headers,
1033
- status: status || 302,
1039
+ status: this.status,
1034
1040
  });
1035
1041
  }
1036
1042
  getResponse() {
@@ -4158,19 +4164,27 @@ class RapidServer {
4158
4164
  const rapidRequest = new RapidRequest(this, request);
4159
4165
  await rapidRequest.parseBody();
4160
4166
  const routeContext = new RouteContext(this, rapidRequest);
4167
+ const { response } = routeContext;
4161
4168
  try {
4162
4169
  await this.#pluginManager.onPrepareRouteContext(routeContext);
4163
4170
  await this.#buildedRoutes(routeContext, next);
4164
4171
  }
4165
4172
  catch (ex) {
4166
4173
  this.#logger.error("handle request error:", ex);
4167
- routeContext.response.json({
4174
+ response.json({
4168
4175
  error: {
4169
4176
  message: ex.message || ex,
4170
4177
  },
4171
4178
  }, 500);
4172
4179
  }
4173
- return routeContext.response.getResponse();
4180
+ if (!response.status && !response.body) {
4181
+ response.json({
4182
+ error: {
4183
+ message: "No route handler was found to handle this request.",
4184
+ },
4185
+ }, 404);
4186
+ }
4187
+ return response.getResponse();
4174
4188
  }
4175
4189
  async beforeRunRouteActions(handlerContext) {
4176
4190
  await this.#pluginManager.beforeRunRouteActions(handlerContext);
@@ -4638,7 +4652,7 @@ function listDataDictionaries(server) {
4638
4652
  async function syncDatabaseSchema(server, applicationConfig) {
4639
4653
  const logger = server.getLogger();
4640
4654
  logger.info("Synchronizing database schema...");
4641
- const sqlQueryTableInformations = `SELECT table_schema, table_name FROM information_schema.tables`;
4655
+ const sqlQueryTableInformations = `SELECT table_schema, table_name, obj_description((table_schema||'.'||quote_ident(table_name))::regclass) as table_description FROM information_schema.tables`;
4642
4656
  const tablesInDb = await server.queryDatabaseObject(sqlQueryTableInformations);
4643
4657
  const { queryBuilder } = server;
4644
4658
  for (const model of applicationConfig.models) {
@@ -4649,9 +4663,14 @@ async function syncDatabaseSchema(server, applicationConfig) {
4649
4663
  if (!tableInDb) {
4650
4664
  await server.queryDatabaseObject(`CREATE TABLE IF NOT EXISTS ${queryBuilder.quoteTable(model)} ()`, []);
4651
4665
  }
4666
+ if (!tableInDb || tableInDb.table_description != model.name) {
4667
+ await server.queryDatabaseObject(`COMMENT ON TABLE ${queryBuilder.quoteTable(model)} IS ${queryBuilder.formatValueToSqlLiteral(model.name)};`, []);
4668
+ }
4652
4669
  }
4653
- const sqlQueryColumnInformations = `SELECT table_schema, table_name, column_name, data_type, udt_name, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale
4654
- FROM information_schema.columns;`;
4670
+ const sqlQueryColumnInformations = `SELECT c.table_schema, c.table_name, c.column_name, c.ordinal_position, d.description, c.data_type, c.udt_name, c.is_nullable, c.column_default, c.character_maximum_length, c.numeric_precision, c.numeric_scale
4671
+ FROM information_schema.columns c
4672
+ INNER JOIN pg_catalog.pg_statio_all_tables st ON (st.schemaname = c.table_schema and st.relname = c.table_name)
4673
+ LEFT JOIN pg_catalog.pg_description d ON (d.objoid = st.relid and d.objsubid = c.ordinal_position);`;
4655
4674
  const columnsInDb = await server.queryDatabaseObject(sqlQueryColumnInformations, []);
4656
4675
  for (const model of applicationConfig.models) {
4657
4676
  logger.debug(`Checking data columns for '${model.namespace}.${model.singularCode}'...`);
@@ -4678,6 +4697,9 @@ async function syncDatabaseSchema(server, applicationConfig) {
4678
4697
  notNull: property.required,
4679
4698
  });
4680
4699
  }
4700
+ if (!columnInDb || columnInDb.description != property.name) {
4701
+ await server.queryDatabaseObject(`COMMENT ON COLUMN ${queryBuilder.quoteTable(model)}.${queryBuilder.quoteObject(property.targetIdColumnName)} IS ${queryBuilder.formatValueToSqlLiteral(property.name)};`, []);
4702
+ }
4681
4703
  }
4682
4704
  else if (property.relation === "many") {
4683
4705
  if (property.linkTableName) {
@@ -4780,6 +4802,9 @@ async function syncDatabaseSchema(server, applicationConfig) {
4780
4802
  }
4781
4803
  }
4782
4804
  }
4805
+ if (!columnInDb || columnInDb.description != property.name) {
4806
+ await server.queryDatabaseObject(`COMMENT ON COLUMN ${queryBuilder.quoteTable(model)}.${queryBuilder.quoteObject(property.columnName || property.code)} IS ${queryBuilder.formatValueToSqlLiteral(property.name)};`, []);
4807
+ }
4783
4808
  }
4784
4809
  }
4785
4810
  }
@@ -4975,7 +5000,13 @@ async function handler$q(plugin, ctx, options) {
4975
5000
  routeContext,
4976
5001
  });
4977
5002
  if (!entity) {
4978
- throw new Error(`${options.namespace}.${options.singularCode} with id "${id}" was not found.`);
5003
+ ctx.routerContext.response.json({
5004
+ error: {
5005
+ message: `${options.namespace}.${options.singularCode} with id "${id}" was not found.`,
5006
+ },
5007
+ }, 404);
5008
+ // routerContext.json() function will not be called if the ctx.output is null or undefined.
5009
+ return;
4979
5010
  }
4980
5011
  return entity;
4981
5012
  });
@@ -1,4 +1,4 @@
1
- import { RpdDataModel, CreateEntityOptions, QuoteTableOptions, DatabaseQuery } from "../types";
1
+ import { RpdDataModel, CreateEntityOptions, QuoteTableOptions, DatabaseQuery, IQueryBuilder } from "../types";
2
2
  import { CountRowOptions, DeleteRowOptions, FindRowOptions, RowFilterOptions, UpdateRowOptions, ColumnSelectOptions } from "../dataAccess/dataAccessTypes";
3
3
  export interface BuildQueryContext {
4
4
  model: RpdDataModel;
@@ -13,12 +13,13 @@ export interface BuildQueryContext {
13
13
  export interface InitQueryBuilderOptions {
14
14
  dbDefaultSchema: string;
15
15
  }
16
- export default class QueryBuilder {
16
+ export default class QueryBuilder implements IQueryBuilder {
17
17
  #private;
18
18
  constructor(options: InitQueryBuilderOptions);
19
19
  quoteTable(options: QuoteTableOptions): string;
20
20
  quoteObject(name: string): string;
21
21
  quoteColumn(model: RpdDataModel, column: ColumnSelectOptions, emitTableAlias: boolean): string;
22
+ formatValueToSqlLiteral(value: any): string;
22
23
  select(model: RpdDataModel, options: FindRowOptions): DatabaseQuery;
23
24
  selectDerived(derivedModel: RpdDataModel, baseModel: RpdDataModel, options: FindRowOptions): DatabaseQuery;
24
25
  count(model: RpdDataModel, options: CountRowOptions): DatabaseQuery;
package/dist/types.d.ts CHANGED
@@ -154,6 +154,7 @@ export interface IQueryBuilder {
154
154
  quoteTable: (options: QuoteTableOptions) => string;
155
155
  quoteObject: (name: string) => string;
156
156
  buildFiltersExpression(model: RpdDataModel, filters: RowFilterOptions[]): any;
157
+ formatValueToSqlLiteral: (value: any) => string;
157
158
  }
158
159
  export interface RpdApplicationConfig {
159
160
  code?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,7 +15,6 @@ function mergeHeaders(target: Headers, source: HeadersInit) {
15
15
  } else if (isObject(source)) {
16
16
  Object.entries(source).forEach(([key, value]) => target.set(key, value));
17
17
  }
18
- return target;
19
18
  }
20
19
 
21
20
  interface NewResponseOptions {
@@ -32,6 +31,7 @@ function newResponse(options: NewResponseOptions) {
32
31
  }
33
32
 
34
33
  export class RapidResponse {
34
+ // TODO: remove this field.
35
35
  #response: Response;
36
36
  status: number;
37
37
  body: BodyInit;
@@ -53,14 +53,17 @@ export class RapidResponse {
53
53
  if (headers) {
54
54
  mergeHeaders(responseHeaders, headers);
55
55
  }
56
- this.#response = newResponse({ body, status: status || 200, headers: responseHeaders });
56
+ this.status = status || 200;
57
+ this.body = body;
58
+ this.#response = newResponse({ body, status: this.status, headers: responseHeaders });
57
59
  }
58
60
 
59
61
  redirect(location: string, status?: HttpStatus) {
60
62
  this.headers.set("Location", location);
63
+ this.status = status || 302;
61
64
  this.#response = newResponse({
62
65
  headers: this.headers,
63
- status: status || 302,
66
+ status: this.status,
64
67
  });
65
68
  }
66
69
 
@@ -14,7 +14,16 @@ export async function handler(plugin: RapidPlugin, ctx: ActionHandlerContext, op
14
14
  routeContext,
15
15
  });
16
16
  if (!entity) {
17
- throw new Error(`${options.namespace}.${options.singularCode} with id "${id}" was not found.`);
17
+ ctx.routerContext.response.json(
18
+ {
19
+ error: {
20
+ message: `${options.namespace}.${options.singularCode} with id "${id}" was not found.`,
21
+ },
22
+ },
23
+ 404,
24
+ );
25
+ // routerContext.json() function will not be called if the ctx.output is null or undefined.
26
+ return;
18
27
  }
19
28
  return entity;
20
29
  });
@@ -181,12 +181,15 @@ function listDataDictionaries(server: IRpdServer) {
181
181
  type TableInformation = {
182
182
  table_schema: string;
183
183
  table_name: string;
184
+ table_description: string;
184
185
  };
185
186
 
186
187
  type ColumnInformation = {
187
188
  table_schema: string;
188
189
  table_name: string;
189
190
  column_name: string;
191
+ ordinal_position: number;
192
+ description?: string;
190
193
  data_type: string;
191
194
  udt_name: string;
192
195
  is_nullable: "YES" | "NO";
@@ -206,7 +209,7 @@ type ConstraintInformation = {
206
209
  async function syncDatabaseSchema(server: IRpdServer, applicationConfig: RpdApplicationConfig) {
207
210
  const logger = server.getLogger();
208
211
  logger.info("Synchronizing database schema...");
209
- const sqlQueryTableInformations = `SELECT table_schema, table_name FROM information_schema.tables`;
212
+ const sqlQueryTableInformations = `SELECT table_schema, table_name, obj_description((table_schema||'.'||quote_ident(table_name))::regclass) as table_description FROM information_schema.tables`;
210
213
  const tablesInDb: TableInformation[] = await server.queryDatabaseObject(sqlQueryTableInformations);
211
214
  const { queryBuilder } = server;
212
215
 
@@ -219,10 +222,15 @@ async function syncDatabaseSchema(server: IRpdServer, applicationConfig: RpdAppl
219
222
  if (!tableInDb) {
220
223
  await server.queryDatabaseObject(`CREATE TABLE IF NOT EXISTS ${queryBuilder.quoteTable(model)} ()`, []);
221
224
  }
225
+ if (!tableInDb || tableInDb.table_description != model.name) {
226
+ await server.queryDatabaseObject(`COMMENT ON TABLE ${queryBuilder.quoteTable(model)} IS ${queryBuilder.formatValueToSqlLiteral(model.name)};`, []);
227
+ }
222
228
  }
223
229
 
224
- const sqlQueryColumnInformations = `SELECT table_schema, table_name, column_name, data_type, udt_name, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale
225
- FROM information_schema.columns;`;
230
+ const sqlQueryColumnInformations = `SELECT c.table_schema, c.table_name, c.column_name, c.ordinal_position, d.description, c.data_type, c.udt_name, c.is_nullable, c.column_default, c.character_maximum_length, c.numeric_precision, c.numeric_scale
231
+ FROM information_schema.columns c
232
+ INNER JOIN pg_catalog.pg_statio_all_tables st ON (st.schemaname = c.table_schema and st.relname = c.table_name)
233
+ LEFT JOIN pg_catalog.pg_description d ON (d.objoid = st.relid and d.objsubid = c.ordinal_position);`;
226
234
  const columnsInDb: ColumnInformation[] = await server.queryDatabaseObject(sqlQueryColumnInformations, []);
227
235
 
228
236
  for (const model of applicationConfig.models) {
@@ -253,6 +261,15 @@ async function syncDatabaseSchema(server: IRpdServer, applicationConfig: RpdAppl
253
261
  notNull: property.required,
254
262
  });
255
263
  }
264
+
265
+ if (!columnInDb || columnInDb.description != property.name) {
266
+ await server.queryDatabaseObject(
267
+ `COMMENT ON COLUMN ${queryBuilder.quoteTable(model)}.${queryBuilder.quoteObject(
268
+ property.targetIdColumnName,
269
+ )} IS ${queryBuilder.formatValueToSqlLiteral(property.name)};`,
270
+ [],
271
+ );
272
+ }
256
273
  } else if (property.relation === "many") {
257
274
  if (property.linkTableName) {
258
275
  const tableInDb = find(tablesInDb, {
@@ -359,6 +376,15 @@ async function syncDatabaseSchema(server: IRpdServer, applicationConfig: RpdAppl
359
376
  }
360
377
  }
361
378
  }
379
+
380
+ if (!columnInDb || columnInDb.description != property.name) {
381
+ await server.queryDatabaseObject(
382
+ `COMMENT ON COLUMN ${queryBuilder.quoteTable(model)}.${queryBuilder.quoteObject(
383
+ property.columnName || property.code,
384
+ )} IS ${queryBuilder.formatValueToSqlLiteral(property.name)};`,
385
+ [],
386
+ );
387
+ }
362
388
  }
363
389
  }
364
390
  }
@@ -1,5 +1,5 @@
1
1
  import { find, isBoolean, isNull, isNumber, isString, isUndefined } from "lodash";
2
- import { RpdDataModel, RpdDataModelProperty, CreateEntityOptions, QuoteTableOptions, DatabaseQuery } from "../types";
2
+ import { RpdDataModel, RpdDataModelProperty, CreateEntityOptions, QuoteTableOptions, DatabaseQuery, IQueryBuilder } from "../types";
3
3
  import {
4
4
  CountRowOptions,
5
5
  DeleteRowOptions,
@@ -19,8 +19,8 @@ import {
19
19
  } from "~/dataAccess/dataAccessTypes";
20
20
  import { pgPropertyTypeColumnMap } from "~/dataAccess/columnTypeMapper";
21
21
 
22
- const objLeftQuoteChar = "\"";
23
- const objRightQuoteChar = "\"";
22
+ const objLeftQuoteChar = '"';
23
+ const objRightQuoteChar = '"';
24
24
 
25
25
  const relationalOperatorsMap = new Map<RowFilterRelationalOperators, string>([
26
26
  ["eq", "="],
@@ -46,7 +46,7 @@ export interface InitQueryBuilderOptions {
46
46
  dbDefaultSchema: string;
47
47
  }
48
48
 
49
- export default class QueryBuilder {
49
+ export default class QueryBuilder implements IQueryBuilder {
50
50
  #dbDefaultSchema: string;
51
51
 
52
52
  constructor(options: InitQueryBuilderOptions) {
@@ -84,6 +84,10 @@ export default class QueryBuilder {
84
84
  }
85
85
  }
86
86
 
87
+ formatValueToSqlLiteral(value: any) {
88
+ return formatValueToSqlLiteral(value);
89
+ }
90
+
87
91
  select(model: RpdDataModel, options: FindRowOptions): DatabaseQuery {
88
92
  const ctx: BuildQueryContext = {
89
93
  model,
@@ -223,7 +227,7 @@ export default class QueryBuilder {
223
227
  paramToLiteral: false,
224
228
  };
225
229
  let { filters } = options;
226
- let command = "SELECT COUNT(*)::int as \"count\" FROM ";
230
+ let command = 'SELECT COUNT(*)::int as "count" FROM ';
227
231
 
228
232
  command += this.quoteTable(model);
229
233
 
@@ -247,7 +251,7 @@ export default class QueryBuilder {
247
251
  paramToLiteral: false,
248
252
  };
249
253
  let { filters } = options;
250
- let command = "SELECT COUNT(*)::int as \"count\" FROM ";
254
+ let command = 'SELECT COUNT(*)::int as "count" FROM ';
251
255
 
252
256
  command += `${this.quoteTable(derivedModel)} LEFT JOIN ${this.quoteTable(baseModel)} ON ${this.quoteObject(derivedModel.tableName)}.id = ${this.quoteObject(
253
257
  baseModel.tableName,
@@ -498,11 +502,10 @@ function buildRangeFilterQuery(ctx: BuildQueryContext, filter: FindRowRangeFilte
498
502
  ctx.params.push(filter.value[0]);
499
503
  command += `$${ctx.params.length}`;
500
504
 
501
- command += " AND "
505
+ command += " AND ";
502
506
 
503
507
  ctx.params.push(filter.value[1]);
504
508
  command += `$${ctx.params.length}`;
505
-
506
509
  } else {
507
510
  throw new Error(`Filter operator '${filter.operator}' is not supported.`);
508
511
  }
package/src/server.ts CHANGED
@@ -400,13 +400,14 @@ export class RapidServer implements IRpdServer {
400
400
  const rapidRequest = new RapidRequest(this, request);
401
401
  await rapidRequest.parseBody();
402
402
  const routeContext: RouteContext = new RouteContext(this, rapidRequest);
403
+ const { response } = routeContext;
403
404
 
404
405
  try {
405
406
  await this.#pluginManager.onPrepareRouteContext(routeContext);
406
407
  await this.#buildedRoutes(routeContext, next);
407
408
  } catch (ex) {
408
409
  this.#logger.error("handle request error:", ex);
409
- routeContext.response.json(
410
+ response.json(
410
411
  {
411
412
  error: {
412
413
  message: ex.message || ex,
@@ -415,7 +416,18 @@ export class RapidServer implements IRpdServer {
415
416
  500,
416
417
  );
417
418
  }
418
- return routeContext.response.getResponse();
419
+
420
+ if (!response.status && !response.body) {
421
+ response.json(
422
+ {
423
+ error: {
424
+ message: "No route handler was found to handle this request.",
425
+ },
426
+ },
427
+ 404,
428
+ );
429
+ }
430
+ return response.getResponse();
419
431
  }
420
432
 
421
433
  async beforeRunRouteActions(handlerContext: ActionHandlerContext) {
package/src/types.ts CHANGED
@@ -179,6 +179,7 @@ export interface IQueryBuilder {
179
179
  quoteTable: (options: QuoteTableOptions) => string;
180
180
  quoteObject: (name: string) => string;
181
181
  buildFiltersExpression(model: RpdDataModel, filters: RowFilterOptions[]);
182
+ formatValueToSqlLiteral: (value: any) => string;
182
183
  }
183
184
 
184
185
  export interface RpdApplicationConfig {