@ruiapp/rapid-core 0.1.72 → 0.1.74

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.
@@ -14,6 +14,8 @@ import {
14
14
  IRpdDataAccessor,
15
15
  RemoveEntityRelationsOptions,
16
16
  RpdDataModel,
17
+ RpdDataModelIndex,
18
+ RpdDataModelIndexOptions,
17
19
  RpdDataModelProperty,
18
20
  UpdateEntityByIdOptions,
19
21
  } from "~/types";
@@ -22,7 +24,7 @@ import { mapDbRowToEntity, mapEntityToDbRow } from "./entityMapper";
22
24
  import { mapPropertyNameToColumnName } from "./propertyMapper";
23
25
  import { IRpdServer, RapidPlugin } from "~/core/server";
24
26
  import { getEntityPartChanges } from "~/helpers/entityHelpers";
25
- import { filter, find, first, forEach, isArray, isNumber, isObject, isString, keys, map, reject, uniq } from "lodash";
27
+ import { cloneDeep, filter, find, first, forEach, isArray, isNumber, isObject, isString, keys, map, reject, uniq } from "lodash";
26
28
  import {
27
29
  getEntityPropertiesIncludingBase,
28
30
  getEntityProperty,
@@ -35,6 +37,7 @@ import { ColumnSelectOptions, CountRowOptions, FindRowOptions, FindRowOrderByOpt
35
37
  import { newEntityOperationError } from "~/utilities/errorUtility";
36
38
  import { getNowStringWithTimezone } from "~/utilities/timeUtility";
37
39
  import { or } from "xstate";
40
+ import { RouteContext } from "~/core/routeContext";
38
41
 
39
42
  export type FindOneRelationEntitiesOptions = {
40
43
  server: IRpdServer;
@@ -703,6 +706,26 @@ async function createEntity(server: IRpdServer, dataAccessor: IRpdDataAccessor,
703
706
  routeContext,
704
707
  });
705
708
 
709
+ // check unique constraints
710
+ if (!options.postponeUniquenessCheck) {
711
+ if (model.indexes && model.indexes.length) {
712
+ for (const indexConfig of model.indexes) {
713
+ if (!indexConfig.unique) {
714
+ continue;
715
+ }
716
+
717
+ const duplicate = await willEntityDuplicate(server, dataAccessor, {
718
+ routeContext: options.routeContext,
719
+ entityToSave: entity,
720
+ indexConfig,
721
+ });
722
+ if (duplicate) {
723
+ throw new Error(getEntityDuplicatedErrorMessage(server, model, indexConfig));
724
+ }
725
+ }
726
+ }
727
+ }
728
+
706
729
  const oneRelationPropertiesToCreate: RpdDataModelProperty[] = [];
707
730
  const manyRelationPropertiesToCreate: RpdDataModelProperty[] = [];
708
731
  keys(entity).forEach((propertyCode) => {
@@ -936,6 +959,27 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
936
959
 
937
960
  changes = getEntityPartChanges(server, model, entity, entityToSave);
938
961
 
962
+ // check unique constraints
963
+ if (!options.postponeUniquenessCheck) {
964
+ if (model.indexes && model.indexes.length) {
965
+ for (const indexConfig of model.indexes) {
966
+ if (!indexConfig.unique) {
967
+ continue;
968
+ }
969
+
970
+ const duplicate = await willEntityDuplicate(server, dataAccessor, {
971
+ routeContext: options.routeContext,
972
+ entityId: id,
973
+ entityToSave: changes,
974
+ indexConfig,
975
+ });
976
+ if (duplicate) {
977
+ throw new Error(getEntityDuplicatedErrorMessage(server, model, indexConfig));
978
+ }
979
+ }
980
+ }
981
+ }
982
+
939
983
  const oneRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
940
984
  const manyRelationPropertiesToUpdate: RpdDataModelProperty[] = [];
941
985
  keys(changes).forEach((propertyCode) => {
@@ -1134,6 +1178,68 @@ async function updateEntityById(server: IRpdServer, dataAccessor: IRpdDataAccess
1134
1178
  return updatedEntity;
1135
1179
  }
1136
1180
 
1181
+ export type CheckEntityDuplicatedOptions = {
1182
+ routeContext?: RouteContext;
1183
+ entityId?: number;
1184
+ entityToSave: any;
1185
+ indexConfig: RpdDataModelIndex;
1186
+ };
1187
+
1188
+ async function willEntityDuplicate(server: IRpdServer, dataAccessor: IRpdDataAccessor, options: CheckEntityDuplicatedOptions): Promise<boolean> {
1189
+ const { entityId, entityToSave, routeContext, indexConfig } = options;
1190
+
1191
+ let filters: EntityFilterOptions[] = [];
1192
+ if (indexConfig.conditions) {
1193
+ filters = cloneDeep(indexConfig.conditions);
1194
+ }
1195
+
1196
+ for (const propConfig of indexConfig.properties) {
1197
+ let propCode: string;
1198
+ if (isString(propConfig)) {
1199
+ propCode = propConfig;
1200
+ } else {
1201
+ propCode = propConfig.code;
1202
+ }
1203
+
1204
+ if (!entityToSave.hasOwnProperty(propCode)) {
1205
+ // skip duplicate checking when any index prop missing in entityToSave.
1206
+ return false;
1207
+ }
1208
+
1209
+ filters.push({
1210
+ operator: "eq",
1211
+ field: propCode,
1212
+ value: entityToSave[propCode],
1213
+ });
1214
+ }
1215
+
1216
+ const entityInDb = await findEntity(server, dataAccessor, {
1217
+ filters,
1218
+ routeContext,
1219
+ });
1220
+
1221
+ if (entityId) {
1222
+ return entityInDb && entityInDb.Id !== entityId;
1223
+ } else {
1224
+ return !!entityInDb;
1225
+ }
1226
+ }
1227
+
1228
+ function getEntityDuplicatedErrorMessage(server: IRpdServer, model: RpdDataModel, indexConfig: RpdDataModelIndex) {
1229
+ const propertyNames = indexConfig.properties.map((propConfig) => {
1230
+ let propCode: string;
1231
+ if (isString(propConfig)) {
1232
+ propCode = propConfig;
1233
+ } else {
1234
+ propCode = propConfig.code;
1235
+ }
1236
+ const prop = getEntityPropertyByCode(server, model, propCode);
1237
+ return prop.name;
1238
+ });
1239
+
1240
+ return `已存在 ${propertyNames.join(", ")} 相同的记录。`;
1241
+ }
1242
+
1137
1243
  export default class EntityManager<TEntity = any> {
1138
1244
  #server: IRpdServer;
1139
1245
  #dataAccessor: IRpdDataAccessor;
@@ -1,28 +1,33 @@
1
- import path from "path";
2
- import { readFile } from "~/utilities/fsUtility";
3
- import { ActionHandlerContext } from "~/core/actionHandler";
4
- import { RapidPlugin } from "~/core/server";
5
-
6
- export const code = "downloadFile";
7
-
8
- export async function handler(plugin: RapidPlugin, ctx: ActionHandlerContext, options: any) {
9
- const { server, applicationConfig, routerContext, input } = ctx;
10
- const { request, response } = routerContext;
11
-
12
- const dataAccessor = ctx.server.getDataAccessor({
13
- singularCode: "ecm_storage_object",
14
- });
15
-
16
- const storageObject = await dataAccessor.findById(input.fileId);
17
- if (!storageObject) {
18
- ctx.output = { error: new Error("Storage object not found.") };
19
- return;
20
- }
21
-
22
- const fileKey = storageObject.key;
23
- const filePathName = path.join(server.config.localFileStoragePath, fileKey);
24
- const attachmentFileName = input.fileName || path.basename(fileKey);
25
-
26
- response.body = await readFile(filePathName);
27
- response.headers.set("Content-Disposition", `attachment; filename="${encodeURIComponent(attachmentFileName)}"`);
28
- }
1
+ import path from "path";
2
+ import { readFile } from "~/utilities/fsUtility";
3
+ import { ActionHandlerContext } from "~/core/actionHandler";
4
+ import { RapidPlugin } from "~/core/server";
5
+
6
+ export const code = "downloadFile";
7
+
8
+ export async function handler(plugin: RapidPlugin, ctx: ActionHandlerContext, options: any) {
9
+ const { server, applicationConfig, routerContext, input } = ctx;
10
+ const { request, response } = routerContext;
11
+ //TODO: only public files can download by this handler
12
+
13
+ let fileKey: string = input.fileKey;
14
+
15
+ if (!fileKey && input.fileId) {
16
+ const dataAccessor = ctx.server.getDataAccessor({
17
+ singularCode: "ecm_storage_object",
18
+ });
19
+
20
+ const storageObject = await dataAccessor.findById(input.fileId);
21
+ if (!storageObject) {
22
+ ctx.output = { error: new Error("Storage object not found.") };
23
+ return;
24
+ }
25
+
26
+ fileKey = storageObject.key;
27
+ }
28
+ const filePathName = path.join(server.config.localFileStoragePath, fileKey);
29
+ const attachmentFileName = input.fileName || path.basename(fileKey);
30
+
31
+ response.body = await readFile(filePathName);
32
+ response.headers.set("Content-Disposition", `attachment; filename="${encodeURIComponent(attachmentFileName)}"`);
33
+ }
@@ -7,6 +7,7 @@ import {
7
7
  QuoteTableOptions,
8
8
  RpdApplicationConfig,
9
9
  RpdDataModel,
10
+ RpdDataModelIndex,
10
11
  RpdDataModelProperty,
11
12
  RpdDataPropertyTypes,
12
13
  RpdEntityCreateEventPayload,
@@ -24,8 +25,8 @@ import {
24
25
  import * as listMetaModels from "./actionHandlers/listMetaModels";
25
26
  import * as listMetaRoutes from "./actionHandlers/listMetaRoutes";
26
27
  import * as getMetaModelDetail from "./actionHandlers/getMetaModelDetail";
27
- import { find } from "lodash";
28
- import { getEntityPropertiesIncludingBase, isRelationProperty } from "~/helpers/metaHelper";
28
+ import { find, isString, map } from "lodash";
29
+ import { getEntityPropertiesIncludingBase, getEntityPropertyByCode, isOneRelationProperty, isRelationProperty } from "~/helpers/metaHelper";
29
30
 
30
31
  class MetaManager implements RapidPlugin {
31
32
  get code(): string {
@@ -366,6 +367,18 @@ async function syncDatabaseSchema(server: IRpdServer, applicationConfig: RpdAppl
366
367
  );
367
368
  }
368
369
  }
370
+
371
+ // generate indexes
372
+ for (const model of applicationConfig.models) {
373
+ if (!model.indexes || !model.indexes.length) {
374
+ continue;
375
+ }
376
+
377
+ for (const index of model.indexes) {
378
+ const sqlCreateIndex = generateTableIndexDDL(queryBuilder, server, model, index);
379
+ await server.tryQueryDatabaseObject(sqlCreateIndex, []);
380
+ }
381
+ }
369
382
  }
370
383
 
371
384
  function generateCreateColumnDDL(
@@ -422,6 +435,56 @@ function generateLinkTableDDL(
422
435
  return columnDDL;
423
436
  }
424
437
 
438
+ function generateTableIndexDDL(queryBuilder: IQueryBuilder, server: IRpdServer, model: RpdDataModel, index: RpdDataModelIndex) {
439
+ let indexName = index.name;
440
+ if (!indexName) {
441
+ indexName = model.tableName;
442
+ for (const indexProp of index.properties) {
443
+ const propCode = isString(indexProp) ? indexProp : indexProp.code;
444
+ const property = getEntityPropertyByCode(server, model, propCode);
445
+ if (!isRelationProperty(property)) {
446
+ indexName += "_" + property.columnName;
447
+ } else if (isOneRelationProperty(property)) {
448
+ indexName += "_" + property.targetIdColumnName;
449
+ }
450
+ }
451
+ indexName += index.unique ? "_uindex" : "_index";
452
+ }
453
+
454
+ const indexColumns = map(index.properties, (indexProp) => {
455
+ let columnName: string;
456
+ const propCode = isString(indexProp) ? indexProp : indexProp.code;
457
+ const property = getEntityPropertyByCode(server, model, propCode);
458
+ if (!isRelationProperty(property)) {
459
+ columnName = property.columnName;
460
+ } else if (isOneRelationProperty(property)) {
461
+ columnName = property.targetIdColumnName;
462
+ }
463
+
464
+ if (isString(indexProp)) {
465
+ return columnName;
466
+ }
467
+
468
+ if (indexProp.order === "desc") {
469
+ return `${columnName} desc`;
470
+ }
471
+
472
+ return columnName;
473
+ });
474
+
475
+ let ddl = `CREATE ${index.unique ? "UNIQUE" : ""} INDEX ${indexName} `;
476
+ ddl += `ON ${queryBuilder.quoteTable({
477
+ schema: model.schema,
478
+ tableName: model.tableName,
479
+ })} (${indexColumns.join(", ")})`;
480
+
481
+ if (index.conditions) {
482
+ ddl += ` WHERE ${queryBuilder.buildFiltersExpression(model, index.conditions)}`;
483
+ }
484
+
485
+ return ddl;
486
+ }
487
+
425
488
  const pgPropertyTypeColumnMap: Partial<Record<RpdDataPropertyTypes, string>> = {
426
489
  integer: "int4",
427
490
  long: "int8",
@@ -1,4 +1,4 @@
1
- import { find } from "lodash";
1
+ import { find, isBoolean, isNull, isNumber, isString, isUndefined } from "lodash";
2
2
  import { RpdDataModel, RpdDataModelProperty, CreateEntityOptions, QuoteTableOptions, DatabaseQuery } from "../types";
3
3
  import {
4
4
  CountRowOptions,
@@ -32,6 +32,10 @@ export interface BuildQueryContext {
32
32
  builder: QueryBuilder;
33
33
  params: any[];
34
34
  emitTableAlias: boolean;
35
+ /**
36
+ * emit parameter value to sql literal.
37
+ */
38
+ paramToLiteral: boolean;
35
39
  }
36
40
 
37
41
  export interface InitQueryBuilderOptions {
@@ -82,6 +86,7 @@ export default class QueryBuilder {
82
86
  builder: this,
83
87
  params: [],
84
88
  emitTableAlias: true,
89
+ paramToLiteral: false,
85
90
  };
86
91
  let { fields: columns, filters, orderBy, pagination } = options;
87
92
  let command = "SELECT ";
@@ -143,6 +148,7 @@ export default class QueryBuilder {
143
148
  builder: this,
144
149
  params: [],
145
150
  emitTableAlias: true,
151
+ paramToLiteral: false,
146
152
  };
147
153
  let { fields: columns, filters, orderBy, pagination } = options;
148
154
  let command = "SELECT ";
@@ -210,6 +216,7 @@ export default class QueryBuilder {
210
216
  builder: this,
211
217
  params: [],
212
218
  emitTableAlias: false,
219
+ paramToLiteral: false,
213
220
  };
214
221
  let { filters } = options;
215
222
  let command = 'SELECT COUNT(*)::int as "count" FROM ';
@@ -233,6 +240,7 @@ export default class QueryBuilder {
233
240
  builder: this,
234
241
  params: [],
235
242
  emitTableAlias: true,
243
+ paramToLiteral: false,
236
244
  };
237
245
  let { filters } = options;
238
246
  let command = 'SELECT COUNT(*)::int as "count" FROM ';
@@ -259,6 +267,7 @@ export default class QueryBuilder {
259
267
  builder: this,
260
268
  params,
261
269
  emitTableAlias: false,
270
+ paramToLiteral: false,
262
271
  };
263
272
  const { entity } = options;
264
273
  let command = "INSERT INTO ";
@@ -302,6 +311,7 @@ export default class QueryBuilder {
302
311
  builder: this,
303
312
  params,
304
313
  emitTableAlias: false,
314
+ paramToLiteral: false,
305
315
  };
306
316
  let { entity, filters } = options;
307
317
  let command = "UPDATE ";
@@ -349,6 +359,7 @@ export default class QueryBuilder {
349
359
  builder: this,
350
360
  params,
351
361
  emitTableAlias: false,
362
+ paramToLiteral: false,
352
363
  };
353
364
  let { filters } = options;
354
365
  let command = "DELETE FROM ";
@@ -365,6 +376,19 @@ export default class QueryBuilder {
365
376
  params: ctx.params,
366
377
  };
367
378
  }
379
+
380
+ buildFiltersExpression(model: RpdDataModel, filters: RowFilterOptions[]) {
381
+ const params: any[] = [];
382
+ const ctx: BuildQueryContext = {
383
+ model,
384
+ builder: this,
385
+ params,
386
+ emitTableAlias: false,
387
+ paramToLiteral: true,
388
+ };
389
+
390
+ return buildFiltersQuery(ctx, filters);
391
+ }
368
392
  }
369
393
 
370
394
  export function buildFiltersQuery(ctx: BuildQueryContext, filters: RowFilterOptions[]) {
@@ -434,8 +458,13 @@ function buildInFilterQuery(ctx: BuildQueryContext, filter: FindRowSetFilterOpti
434
458
  } else {
435
459
  command += " <> ";
436
460
  }
437
- ctx.params.push(filter.value);
438
- command += `ANY($${ctx.params.length}::${filter.itemType || "int"}[])`;
461
+
462
+ if (ctx.paramToLiteral) {
463
+ // TODO: implement it
464
+ } else {
465
+ ctx.params.push(filter.value);
466
+ command += `ANY($${ctx.params.length}::${filter.itemType || "int"}[])`;
467
+ }
439
468
 
440
469
  return command;
441
470
  }
@@ -444,8 +473,13 @@ function buildContainsFilterQuery(ctx: BuildQueryContext, filter: FindRowRelatio
444
473
  let command = ctx.builder.quoteColumn(ctx.model, filter.field, ctx.emitTableAlias);
445
474
 
446
475
  command += " LIKE ";
447
- ctx.params.push(`%${filter.value}%`);
448
- command += "$" + ctx.params.length;
476
+
477
+ if (ctx.paramToLiteral) {
478
+ // TODO: implement it
479
+ } else {
480
+ ctx.params.push(`%${filter.value}%`);
481
+ command += "$" + ctx.params.length;
482
+ }
449
483
 
450
484
  return command;
451
485
  }
@@ -454,8 +488,12 @@ function buildNotContainsFilterQuery(ctx: BuildQueryContext, filter: FindRowRela
454
488
  let command = ctx.builder.quoteColumn(ctx.model, filter.field, ctx.emitTableAlias);
455
489
 
456
490
  command += " NOT LIKE ";
457
- ctx.params.push(`%${filter.value}%`);
458
- command += "$" + ctx.params.length;
491
+ if (ctx.paramToLiteral) {
492
+ // TODO: implement it
493
+ } else {
494
+ ctx.params.push(`%${filter.value}%`);
495
+ command += "$" + ctx.params.length;
496
+ }
459
497
 
460
498
  return command;
461
499
  }
@@ -464,8 +502,13 @@ function buildStartsWithFilterQuery(ctx: BuildQueryContext, filter: FindRowRelat
464
502
  let command = ctx.builder.quoteColumn(ctx.model, filter.field, ctx.emitTableAlias);
465
503
 
466
504
  command += " LIKE ";
467
- ctx.params.push(`${filter.value}%`);
468
- command += "$" + ctx.params.length;
505
+
506
+ if (ctx.paramToLiteral) {
507
+ // TODO: implement it
508
+ } else {
509
+ ctx.params.push(`${filter.value}%`);
510
+ command += "$" + ctx.params.length;
511
+ }
469
512
 
470
513
  return command;
471
514
  }
@@ -474,8 +517,13 @@ function buildNotStartsWithFilterQuery(ctx: BuildQueryContext, filter: FindRowRe
474
517
  let command = ctx.builder.quoteColumn(ctx.model, filter.field, ctx.emitTableAlias);
475
518
 
476
519
  command += " NOT LIKE ";
477
- ctx.params.push(`${filter.value}%`);
478
- command += "$" + ctx.params.length;
520
+
521
+ if (ctx.paramToLiteral) {
522
+ // TODO: implement it
523
+ } else {
524
+ ctx.params.push(`${filter.value}%`);
525
+ command += "$" + ctx.params.length;
526
+ }
479
527
 
480
528
  return command;
481
529
  }
@@ -484,8 +532,13 @@ function buildEndsWithFilterQuery(ctx: BuildQueryContext, filter: FindRowRelatio
484
532
  let command = ctx.builder.quoteColumn(ctx.model, filter.field, ctx.emitTableAlias);
485
533
 
486
534
  command += " LIKE ";
487
- ctx.params.push(`%${filter.value}`);
488
- command += "$" + ctx.params.length;
535
+
536
+ if (ctx.paramToLiteral) {
537
+ // TODO: implement it
538
+ } else {
539
+ ctx.params.push(`%${filter.value}`);
540
+ command += "$" + ctx.params.length;
541
+ }
489
542
 
490
543
  return command;
491
544
  }
@@ -494,8 +547,13 @@ function buildNotEndsWithFilterQuery(ctx: BuildQueryContext, filter: FindRowRela
494
547
  let command = ctx.builder.quoteColumn(ctx.model, filter.field, ctx.emitTableAlias);
495
548
 
496
549
  command += " NOT LIKE ";
497
- ctx.params.push(`%${filter.value}`);
498
- command += "$" + ctx.params.length;
550
+
551
+ if (ctx.paramToLiteral) {
552
+ // TODO: implement it
553
+ } else {
554
+ ctx.params.push(`%${filter.value}`);
555
+ command += "$" + ctx.params.length;
556
+ }
499
557
 
500
558
  return command;
501
559
  }
@@ -505,8 +563,32 @@ function buildRelationalFilterQuery(ctx: BuildQueryContext, filter: FindRowRelat
505
563
 
506
564
  command += relationalOperatorsMap.get(filter.operator);
507
565
 
508
- ctx.params.push(filter.value);
509
- command += "$" + ctx.params.length;
566
+ if (ctx.paramToLiteral) {
567
+ command += formatValueToSqlLiteral(filter.value);
568
+ } else {
569
+ ctx.params.push(filter.value);
570
+ command += "$" + ctx.params.length;
571
+ }
510
572
 
511
573
  return command;
512
574
  }
575
+
576
+ function formatValueToSqlLiteral(value: any) {
577
+ if (isNull(value) || isUndefined(value)) {
578
+ return "null";
579
+ }
580
+
581
+ if (isString(value)) {
582
+ return `'${value.replaceAll("'", "''")}'`;
583
+ }
584
+
585
+ if (isBoolean(value)) {
586
+ return value ? "true" : "false";
587
+ }
588
+
589
+ if (isNumber(value)) {
590
+ return value.toString();
591
+ }
592
+
593
+ return `'${value.toString().replaceAll("'", "''")}'`;
594
+ }