@solidxai/core 0.1.11-beta.8 → 0.1.11
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/CHANGELOG.md +456 -0
- package/dist/controllers/datasource-introspection.controller.d.ts +8 -0
- package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.js +0 -2
- package/dist/controllers/media.controller.js.map +1 -1
- package/dist/helpers/module-metadata-helper.service.js +1 -1
- package/dist/helpers/module-metadata-helper.service.js.map +1 -1
- package/dist/helpers/solid-registry.d.ts +1 -2
- package/dist/helpers/solid-registry.d.ts.map +1 -1
- package/dist/helpers/solid-registry.js.map +1 -1
- package/dist/helpers/typeorm-db-helper.d.ts +1 -0
- package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
- package/dist/helpers/typeorm-db-helper.js +12 -0
- package/dist/helpers/typeorm-db-helper.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +1 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +62 -33
- package/dist/services/authentication.service.d.ts +2 -0
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +32 -11
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +5 -6
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +32 -12
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/datasource-introspection.service.d.ts +10 -0
- package/dist/services/datasource-introspection.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection.service.js +109 -17
- package/dist/services/datasource-introspection.service.js.map +1 -1
- package/dist/services/menu-item-metadata.service.d.ts.map +1 -1
- package/dist/services/menu-item-metadata.service.js +1 -0
- package/dist/services/menu-item-metadata.service.js.map +1 -1
- package/dist/services/model-metadata.service.d.ts +2 -0
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +49 -15
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/module-metadata.service.d.ts.map +1 -1
- package/dist/services/module-metadata.service.js +0 -27
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +212 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +106 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/solid-ts-morph.service.d.ts +6 -0
- package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
- package/dist/services/solid-ts-morph.service.js +43 -0
- package/dist/services/solid-ts-morph.service.js.map +1 -1
- package/dist/subscribers/audit.subscriber.d.ts +0 -1
- package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
- package/dist/subscribers/audit.subscriber.js +1 -10
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.js +11 -13
- package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
- package/dist/subscribers/scheduled-job.subscriber.js +1 -1
- package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
- package/dist/subscribers/security-rule.subscriber.js +1 -1
- package/dist/subscribers/security-rule.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/media.controller.ts +1 -1
- package/src/helpers/module-metadata-helper.service.ts +1 -1
- package/src/helpers/solid-registry.ts +1 -7
- package/src/helpers/typeorm-db-helper.ts +15 -0
- package/src/index.ts +0 -1
- package/src/interfaces.ts +1 -0
- package/src/seeders/seed-data/solid-core-metadata.json +62 -33
- package/src/services/authentication.service.ts +46 -12
- package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +5 -10
- package/src/services/crud-helper.service.ts +32 -12
- package/src/services/datasource-introspection.service.ts +176 -16
- package/src/services/menu-item-metadata.service.ts +2 -1
- package/src/services/model-metadata.service.ts +63 -21
- package/src/services/module-metadata.service.ts +0 -35
- package/src/services/settings/default-settings-provider.service.ts +106 -0
- package/src/services/solid-ts-morph.service.ts +61 -1
- package/src/subscribers/audit.subscriber.ts +1 -12
- package/src/subscribers/computed-entity-field.subscriber.ts +23 -29
- package/src/subscribers/created-by-updated-by.subscriber.ts +0 -3
- package/src/subscribers/field-metadata.subscriber.ts +0 -3
- package/src/subscribers/scheduled-job.subscriber.ts +1 -1
- package/src/subscribers/security-rule.subscriber.ts +1 -1
- package/dist/helpers/nodemon-heartbeat.d.ts +0 -5
- package/dist/helpers/nodemon-heartbeat.d.ts.map +0 -1
- package/dist/helpers/nodemon-heartbeat.js +0 -67
- package/dist/helpers/nodemon-heartbeat.js.map +0 -1
- package/src/helpers/nodemon-heartbeat.ts +0 -59
|
@@ -5,6 +5,7 @@ import { ActiveUserData } from "src/interfaces/active-user-data.interface";
|
|
|
5
5
|
import { SolidRegistry } from "src/helpers/solid-registry";
|
|
6
6
|
import { BadRequestException, Logger } from "@nestjs/common";
|
|
7
7
|
import { ERROR_MESSAGES } from "src/constants/error-messages";
|
|
8
|
+
import { buildCastToText } from "src/helpers/typeorm-db-helper";
|
|
8
9
|
|
|
9
10
|
export enum FilterCombinator {
|
|
10
11
|
AND = '$and',
|
|
@@ -101,15 +102,19 @@ export class CrudHelperService {
|
|
|
101
102
|
case '$eq':
|
|
102
103
|
qb.andWhere(`${colExpr} = :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$eq });
|
|
103
104
|
break;
|
|
104
|
-
case '$eqi':
|
|
105
|
-
|
|
105
|
+
case '$eqi': {
|
|
106
|
+
const castExpr = buildCastToText(this.getDriver(qb), colExpr);
|
|
107
|
+
qb.andWhere(`LOWER(${castExpr}) = :${uniqueFieldAlias}`, { [uniqueFieldAlias]: String(normalizedPrimaryOperatorObj.$eqi).toLowerCase() });
|
|
106
108
|
break;
|
|
109
|
+
}
|
|
107
110
|
case '$ne':
|
|
108
111
|
qb.andWhere(`${colExpr} != :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$ne });
|
|
109
112
|
break;
|
|
110
|
-
case '$nei':
|
|
111
|
-
|
|
113
|
+
case '$nei': {
|
|
114
|
+
const castExpr = buildCastToText(this.getDriver(qb), colExpr);
|
|
115
|
+
qb.andWhere(`LOWER(${castExpr}) != :${uniqueFieldAlias}`, { [uniqueFieldAlias]: String(normalizedPrimaryOperatorObj.$nei).toLowerCase() });
|
|
112
116
|
break;
|
|
117
|
+
}
|
|
113
118
|
case '$gt':
|
|
114
119
|
qb.andWhere(`${colExpr} > :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$gt });
|
|
115
120
|
break;
|
|
@@ -134,12 +139,16 @@ export class CrudHelperService {
|
|
|
134
139
|
case '$notContains':
|
|
135
140
|
qb.andWhere(`${colExpr} NOT LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${normalizedPrimaryOperatorObj.$notContains}%` });
|
|
136
141
|
break;
|
|
137
|
-
case '$containsi':
|
|
138
|
-
|
|
142
|
+
case '$containsi': {
|
|
143
|
+
const castExpr = buildCastToText(this.getDriver(qb), colExpr);
|
|
144
|
+
qb.andWhere(`LOWER(${castExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${String(normalizedPrimaryOperatorObj.$containsi).toLowerCase()}%` });
|
|
139
145
|
break;
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
}
|
|
147
|
+
case '$notContainsi': {
|
|
148
|
+
const castExpr = buildCastToText(this.getDriver(qb), colExpr);
|
|
149
|
+
qb.andWhere(`LOWER(${castExpr}) NOT LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${String(normalizedPrimaryOperatorObj.$notContainsi).toLowerCase()}%` });
|
|
142
150
|
break;
|
|
151
|
+
}
|
|
143
152
|
case '$null':
|
|
144
153
|
qb.andWhere(`${colExpr} IS NULL`);
|
|
145
154
|
break;
|
|
@@ -152,15 +161,19 @@ export class CrudHelperService {
|
|
|
152
161
|
case '$startsWith':
|
|
153
162
|
qb.andWhere(`${colExpr} LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `${normalizedPrimaryOperatorObj.$startsWith}%` });
|
|
154
163
|
break;
|
|
155
|
-
case '$startsWithi':
|
|
156
|
-
|
|
164
|
+
case '$startsWithi': {
|
|
165
|
+
const castExpr = buildCastToText(this.getDriver(qb), colExpr);
|
|
166
|
+
qb.andWhere(`LOWER(${castExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `${String(normalizedPrimaryOperatorObj.$startsWithi).toLowerCase()}%` });
|
|
157
167
|
break;
|
|
168
|
+
}
|
|
158
169
|
case '$endsWith':
|
|
159
170
|
qb.andWhere(`${colExpr} LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${normalizedPrimaryOperatorObj.$endsWith}` });
|
|
160
171
|
break;
|
|
161
|
-
case '$endsWithi':
|
|
162
|
-
|
|
172
|
+
case '$endsWithi': {
|
|
173
|
+
const castExpr = buildCastToText(this.getDriver(qb), colExpr);
|
|
174
|
+
qb.andWhere(`LOWER(${castExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${String(normalizedPrimaryOperatorObj.$endsWithi).toLowerCase()}` });
|
|
163
175
|
break;
|
|
176
|
+
}
|
|
164
177
|
default:
|
|
165
178
|
throw new Error(`Operator ${operator} is not supported`);
|
|
166
179
|
}
|
|
@@ -280,9 +293,13 @@ export class CrudHelperService {
|
|
|
280
293
|
const orderOptions = this.orderOptions(normalizedSort);
|
|
281
294
|
if (orderOptions) {
|
|
282
295
|
const orderOptionKeys = Object.keys(orderOptions) as Array<keyof typeof orderOptions>;
|
|
296
|
+
let hasExplicitIdSort = false;
|
|
283
297
|
orderOptionKeys.forEach((key) => {
|
|
284
298
|
const value = orderOptions[key] as 'ASC' | 'DESC';
|
|
285
299
|
const field = String(key);
|
|
300
|
+
if (field === 'id') {
|
|
301
|
+
hasExplicitIdSort = true;
|
|
302
|
+
}
|
|
286
303
|
if (field.includes('.')) {
|
|
287
304
|
const { alias, property, created } = this.ensureRelationPathJoined(qb, entityAlias, field.split('.'));
|
|
288
305
|
const orderColumn = `${alias}.${property}`;
|
|
@@ -292,6 +309,9 @@ export class CrudHelperService {
|
|
|
292
309
|
qb.addOrderBy(`${entityAlias}.${field}`, value);
|
|
293
310
|
}
|
|
294
311
|
});
|
|
312
|
+
if (!hasExplicitIdSort) {
|
|
313
|
+
qb.addOrderBy(`${entityAlias}.id`, 'DESC');
|
|
314
|
+
}
|
|
295
315
|
}
|
|
296
316
|
}
|
|
297
317
|
|
|
@@ -43,6 +43,10 @@ type TableInventoryRecord = {
|
|
|
43
43
|
suggestedUserKeyField: string;
|
|
44
44
|
columnCount: number;
|
|
45
45
|
physicalColumnCount: number;
|
|
46
|
+
hasPrimaryKey: boolean;
|
|
47
|
+
primaryKeyColumnCount: number;
|
|
48
|
+
primaryKeyColumnNames: string[];
|
|
49
|
+
mappingBlockedReason: string | null;
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
type TableDetailColumnRecord = {
|
|
@@ -401,10 +405,7 @@ export class DatasourceIntrospectionService {
|
|
|
401
405
|
throw new BadRequestException("Persist the model mapping first so the generated entity can be registered before creating migration artifacts.");
|
|
402
406
|
}
|
|
403
407
|
|
|
404
|
-
|
|
405
|
-
await fs.mkdir(path.dirname(plan.migration.filePath), { recursive: true });
|
|
406
|
-
await fs.writeFile(plan.migration.filePath, plan.migration.content, "utf8");
|
|
407
|
-
}
|
|
408
|
+
const migrationWritten = await this.writeMigrationFile(plan.migration);
|
|
408
409
|
|
|
409
410
|
const datasourceFile = await this.syncTypeormDatasourceFile(
|
|
410
411
|
module.name,
|
|
@@ -418,7 +419,7 @@ export class DatasourceIntrospectionService {
|
|
|
418
419
|
operation: plan.operation,
|
|
419
420
|
migration: {
|
|
420
421
|
...plan.migration,
|
|
421
|
-
written:
|
|
422
|
+
written: migrationWritten,
|
|
422
423
|
},
|
|
423
424
|
datasourceFile,
|
|
424
425
|
summary: plan.summary,
|
|
@@ -504,6 +505,8 @@ export class DatasourceIntrospectionService {
|
|
|
504
505
|
if (!tableRecord) {
|
|
505
506
|
throw new NotFoundException(`Table "${mappingDto.tableName}" was not found on datasource "${datasource.name}".`);
|
|
506
507
|
}
|
|
508
|
+
const tableColumns = inventory.columnsByKey.get(tableKey) ?? [];
|
|
509
|
+
this.validateTablePrimaryKeyForMapping(tableRecord, tableColumns, mappingDto);
|
|
507
510
|
|
|
508
511
|
const existingModel = mappingDto.mapped && mappingDto.modelId
|
|
509
512
|
? await this.modelMetadataRepository.findOne({
|
|
@@ -521,14 +524,14 @@ export class DatasourceIntrospectionService {
|
|
|
521
524
|
throw new NotFoundException(`Mapped model "${mappingDto.modelId}" was not found.`);
|
|
522
525
|
}
|
|
523
526
|
|
|
524
|
-
const reviewedModel = this.normalizeReviewedMetadataModel(mappingDto.reviewedModel, mappingDto);
|
|
527
|
+
const reviewedModel = this.normalizeReviewedMetadataModel(mappingDto.reviewedModel, mappingDto, tableColumns);
|
|
525
528
|
const modelPayload = reviewedModel
|
|
526
529
|
? this.buildModelPayloadFromReviewedModel(moduleId, mappingDto, existingModel, reviewedModel)
|
|
527
530
|
: this.buildModelPayload(moduleId, mappingDto, existingModel);
|
|
528
531
|
const metadataJson = reviewedModel
|
|
529
532
|
? await this.buildMetadataJsonPreviewFromReviewedModel(module.name, reviewedModel)
|
|
530
533
|
: await this.buildMetadataJsonPreview(module.name, mappingDto, existingModel);
|
|
531
|
-
const migration = await this.buildMigrationPreview(module.name, datasource, mappingDto,
|
|
534
|
+
const migration = await this.buildMigrationPreview(module.name, datasource, mappingDto, tableColumns);
|
|
532
535
|
const includedColumnNames = mappingDto.columns
|
|
533
536
|
.filter((column) => !column.handledBySuperclass && column.include)
|
|
534
537
|
.map((column) => column.columnName);
|
|
@@ -609,6 +612,11 @@ export class DatasourceIntrospectionService {
|
|
|
609
612
|
existingModel: ModelMetadata | null,
|
|
610
613
|
reviewedModel: ReviewedMetadataModel,
|
|
611
614
|
): CreateModelMetadataDto | UpdateModelMetaDataDto {
|
|
615
|
+
const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
|
|
616
|
+
reviewedModel.fields,
|
|
617
|
+
reviewedModel.userKeyFieldUserKey,
|
|
618
|
+
mappingDto.userKeyField,
|
|
619
|
+
);
|
|
612
620
|
const systemFieldPayloads = existingModel
|
|
613
621
|
? existingModel.fields
|
|
614
622
|
.filter((field) => field.isSystem)
|
|
@@ -620,7 +628,7 @@ export class DatasourceIntrospectionService {
|
|
|
620
628
|
return {
|
|
621
629
|
...field,
|
|
622
630
|
...(matchedExistingField?.id ? { id: matchedExistingField.id } : {}),
|
|
623
|
-
isUserKey: field.name ===
|
|
631
|
+
isUserKey: field.name === resolvedUserKeyFieldName,
|
|
624
632
|
isSystem: field.isSystem ?? false,
|
|
625
633
|
};
|
|
626
634
|
});
|
|
@@ -654,6 +662,11 @@ export class DatasourceIntrospectionService {
|
|
|
654
662
|
) {
|
|
655
663
|
const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
|
|
656
664
|
const previewFields = await this.buildMetadataPreviewFields(mappingDto, existingModel);
|
|
665
|
+
const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
|
|
666
|
+
previewFields,
|
|
667
|
+
mappingDto.userKeyField,
|
|
668
|
+
mappingDto.userKeyField,
|
|
669
|
+
) ?? mappingDto.userKeyField;
|
|
657
670
|
|
|
658
671
|
return {
|
|
659
672
|
filePath,
|
|
@@ -665,7 +678,7 @@ export class DatasourceIntrospectionService {
|
|
|
665
678
|
dataSource: mappingDto.dataSource,
|
|
666
679
|
dataSourceType: mappingDto.dataSourceType,
|
|
667
680
|
tableName: mappingDto.tableName,
|
|
668
|
-
userKeyFieldUserKey:
|
|
681
|
+
userKeyFieldUserKey: resolvedUserKeyFieldName,
|
|
669
682
|
isChild: false,
|
|
670
683
|
legacyTableType: mappingDto.legacyTableType,
|
|
671
684
|
parentModelUserKey: "",
|
|
@@ -733,10 +746,6 @@ export class DatasourceIntrospectionService {
|
|
|
733
746
|
|
|
734
747
|
const serialized = Array.from(finalFields.values())
|
|
735
748
|
.filter((field) => !field.isSystem)
|
|
736
|
-
.map((field) => ({
|
|
737
|
-
...field,
|
|
738
|
-
isUserKey: field.name === mappingDto.userKeyField,
|
|
739
|
-
}))
|
|
740
749
|
.sort((left, right) => {
|
|
741
750
|
const leftOrder = typeof left.id === "number" ? left.id : Number.MAX_SAFE_INTEGER;
|
|
742
751
|
const rightOrder = typeof right.id === "number" ? right.id : Number.MAX_SAFE_INTEGER;
|
|
@@ -752,6 +761,16 @@ export class DatasourceIntrospectionService {
|
|
|
752
761
|
previewFields.push(await this.fieldMetadataService.createFieldConfig(field as any));
|
|
753
762
|
}
|
|
754
763
|
|
|
764
|
+
const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
|
|
765
|
+
previewFields,
|
|
766
|
+
mappingDto.userKeyField,
|
|
767
|
+
mappingDto.userKeyField,
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
for (const field of previewFields) {
|
|
771
|
+
field.isUserKey = field.name === resolvedUserKeyFieldName;
|
|
772
|
+
}
|
|
773
|
+
|
|
755
774
|
return previewFields;
|
|
756
775
|
}
|
|
757
776
|
|
|
@@ -829,6 +848,7 @@ export class DatasourceIntrospectionService {
|
|
|
829
848
|
private normalizeReviewedMetadataModel(
|
|
830
849
|
reviewedModel: Record<string, any> | undefined,
|
|
831
850
|
mappingDto: DatasourceIntrospectionMappingDto,
|
|
851
|
+
tableColumns: DatasourceIntrospectionColumn[],
|
|
832
852
|
): ReviewedMetadataModel | null {
|
|
833
853
|
if (!reviewedModel) {
|
|
834
854
|
return null;
|
|
@@ -888,11 +908,24 @@ export class DatasourceIntrospectionService {
|
|
|
888
908
|
fieldNames.add(loweredName);
|
|
889
909
|
}
|
|
890
910
|
|
|
891
|
-
const
|
|
892
|
-
|
|
911
|
+
const requestedUserKeyFieldUserKey = readString("userKeyFieldUserKey");
|
|
912
|
+
const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
|
|
913
|
+
normalizedFields,
|
|
914
|
+
requestedUserKeyFieldUserKey,
|
|
915
|
+
mappingDto.userKeyField,
|
|
916
|
+
);
|
|
917
|
+
const inheritedGeneratedIdUserKey = this.isGeneratedIdInheritedUserKey(
|
|
918
|
+
mappingDto.legacyTableType,
|
|
919
|
+
requestedUserKeyFieldUserKey,
|
|
920
|
+
);
|
|
921
|
+
if (!resolvedUserKeyFieldName && !inheritedGeneratedIdUserKey) {
|
|
893
922
|
throw new BadRequestException("Reviewed metadata JSON must keep the selected userKeyFieldUserKey in the fields array.");
|
|
894
923
|
}
|
|
895
924
|
|
|
925
|
+
this.validateReviewedPrimaryKeyFields(normalizedFields, tableColumns);
|
|
926
|
+
|
|
927
|
+
const userKeyFieldUserKey = resolvedUserKeyFieldName ?? requestedUserKeyFieldUserKey;
|
|
928
|
+
|
|
896
929
|
return {
|
|
897
930
|
singularName: readString("singularName"),
|
|
898
931
|
pluralName: readString("pluralName"),
|
|
@@ -911,11 +944,54 @@ export class DatasourceIntrospectionService {
|
|
|
911
944
|
internationalisation: readBoolean("internationalisation"),
|
|
912
945
|
fields: normalizedFields.map((field) => ({
|
|
913
946
|
...field,
|
|
914
|
-
isUserKey: field.name ===
|
|
947
|
+
isUserKey: field.name === resolvedUserKeyFieldName,
|
|
915
948
|
})),
|
|
916
949
|
};
|
|
917
950
|
}
|
|
918
951
|
|
|
952
|
+
private resolveReviewedUserKeyFieldName(
|
|
953
|
+
reviewedFields: Array<Record<string, any>>,
|
|
954
|
+
requestedUserKeyFieldName: string,
|
|
955
|
+
fallbackUserKeyFieldName?: string,
|
|
956
|
+
) {
|
|
957
|
+
const activeFields = reviewedFields.filter((field) => !field.isMarkedForRemoval);
|
|
958
|
+
const normalizedRequested = `${requestedUserKeyFieldName ?? ""}`.trim().toLowerCase();
|
|
959
|
+
const normalizedFallback = `${fallbackUserKeyFieldName ?? ""}`.trim().toLowerCase();
|
|
960
|
+
|
|
961
|
+
const byExactRequested = activeFields.find((field) => field.name?.toLowerCase?.() === normalizedRequested);
|
|
962
|
+
if (byExactRequested) {
|
|
963
|
+
return byExactRequested.name;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const byExistingUserKeyFlag = activeFields.find((field) => field.isUserKey);
|
|
967
|
+
if (byExistingUserKeyFlag) {
|
|
968
|
+
return byExistingUserKeyFlag.name;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const byPrimaryKeyFallback = activeFields.find((field) => {
|
|
972
|
+
if (!field.isPrimaryKey) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const normalizedName = `${field.name ?? ""}`.trim().toLowerCase();
|
|
977
|
+
return normalizedName === normalizedFallback || normalizedName === "id" || normalizedName === "legacyid";
|
|
978
|
+
});
|
|
979
|
+
if (byPrimaryKeyFallback) {
|
|
980
|
+
return byPrimaryKeyFallback.name;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const byFallbackFieldName = activeFields.find((field) => field.name?.toLowerCase?.() === normalizedFallback);
|
|
984
|
+
return byFallbackFieldName?.name ?? null;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private isGeneratedIdInheritedUserKey(
|
|
988
|
+
legacyTableType: LegacyTableType,
|
|
989
|
+
requestedUserKeyFieldName: string | null | undefined,
|
|
990
|
+
) {
|
|
991
|
+
return legacyTableType === LegacyTableType.GENERATED_ID
|
|
992
|
+
&& `${requestedUserKeyFieldName ?? ""}`.trim().toLowerCase() === "id";
|
|
993
|
+
}
|
|
994
|
+
|
|
919
995
|
private normalizeReviewedField(field: any, index: number) {
|
|
920
996
|
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
921
997
|
throw new BadRequestException(`Reviewed metadata JSON field at index ${index} is not a valid object.`);
|
|
@@ -1309,6 +1385,8 @@ export class DatasourceIntrospectionService {
|
|
|
1309
1385
|
const suggestedIdColumn = this.getSuggestedIdColumn(datasource.type, tableColumns, suggestedLegacyTableType);
|
|
1310
1386
|
const mappedModel = this.resolveMappedModel(modelLookup, datasource.name, tableName);
|
|
1311
1387
|
const physicalColumns = tableColumns.filter((column) => !this.isHandledBySuperclass(column.columnName, suggestedLegacyTableType));
|
|
1388
|
+
const primaryKeyColumns = tableColumns.filter((column) => column.isPrimaryKey);
|
|
1389
|
+
const hasPrimaryKey = primaryKeyColumns.length > 0;
|
|
1312
1390
|
|
|
1313
1391
|
return {
|
|
1314
1392
|
schema,
|
|
@@ -1326,6 +1404,12 @@ export class DatasourceIntrospectionService {
|
|
|
1326
1404
|
suggestedUserKeyField: this.buildUserKeyCandidates(tableColumns)[0]?.name ?? "id",
|
|
1327
1405
|
columnCount: tableColumns.length,
|
|
1328
1406
|
physicalColumnCount: physicalColumns.length,
|
|
1407
|
+
hasPrimaryKey,
|
|
1408
|
+
primaryKeyColumnCount: primaryKeyColumns.length,
|
|
1409
|
+
primaryKeyColumnNames: primaryKeyColumns.map((column) => column.columnName),
|
|
1410
|
+
mappingBlockedReason: hasPrimaryKey
|
|
1411
|
+
? null
|
|
1412
|
+
: this.buildMissingPrimaryKeyMessage(tableName),
|
|
1329
1413
|
} satisfies TableInventoryRecord;
|
|
1330
1414
|
})
|
|
1331
1415
|
.filter((record): record is TableInventoryRecord => Boolean(record))
|
|
@@ -1530,6 +1614,66 @@ export class DatasourceIntrospectionService {
|
|
|
1530
1614
|
};
|
|
1531
1615
|
}
|
|
1532
1616
|
|
|
1617
|
+
private validateTablePrimaryKeyForMapping(
|
|
1618
|
+
tableRecord: TableInventoryRecord,
|
|
1619
|
+
tableColumns: DatasourceIntrospectionColumn[],
|
|
1620
|
+
mappingDto: DatasourceIntrospectionMappingDto,
|
|
1621
|
+
) {
|
|
1622
|
+
const detectedPrimaryKeyColumns = tableColumns.filter((column) => column.isPrimaryKey);
|
|
1623
|
+
if (!detectedPrimaryKeyColumns.length) {
|
|
1624
|
+
throw new BadRequestException(tableRecord.mappingBlockedReason ?? this.buildMissingPrimaryKeyMessage(tableRecord.tableName));
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const requestColumnsByName = new Map(
|
|
1628
|
+
mappingDto.columns.map((column) => [column.columnName.trim().toLowerCase(), column]),
|
|
1629
|
+
);
|
|
1630
|
+
|
|
1631
|
+
for (const primaryKeyColumn of detectedPrimaryKeyColumns) {
|
|
1632
|
+
const matchingRequestColumn = requestColumnsByName.get(primaryKeyColumn.columnName.trim().toLowerCase());
|
|
1633
|
+
if (!matchingRequestColumn) {
|
|
1634
|
+
throw new BadRequestException(`Primary key column "${primaryKeyColumn.columnName}" must remain present in the mapping request for table "${tableRecord.tableName}".`);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (!matchingRequestColumn.include) {
|
|
1638
|
+
throw new BadRequestException(`Primary key column "${primaryKeyColumn.columnName}" cannot be excluded from the mapping for table "${tableRecord.tableName}".`);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (!matchingRequestColumn.isPrimaryKey) {
|
|
1642
|
+
throw new BadRequestException(`Primary key column "${primaryKeyColumn.columnName}" must stay marked as isPrimaryKey for table "${tableRecord.tableName}".`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
private validateReviewedPrimaryKeyFields(
|
|
1648
|
+
reviewedFields: Array<Record<string, any>>,
|
|
1649
|
+
tableColumns: DatasourceIntrospectionColumn[],
|
|
1650
|
+
) {
|
|
1651
|
+
const detectedPrimaryKeyColumns = tableColumns.filter((column) => column.isPrimaryKey);
|
|
1652
|
+
if (!detectedPrimaryKeyColumns.length) {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const activeReviewedFields = reviewedFields.filter((field) => !field.isMarkedForRemoval);
|
|
1657
|
+
|
|
1658
|
+
for (const primaryKeyColumn of detectedPrimaryKeyColumns) {
|
|
1659
|
+
const matchingReviewedField = activeReviewedFields.find((field) =>
|
|
1660
|
+
`${field.columnName ?? ""}`.trim().toLowerCase() === primaryKeyColumn.columnName.trim().toLowerCase(),
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
if (!matchingReviewedField) {
|
|
1664
|
+
throw new BadRequestException(`Reviewed metadata JSON must keep datasource primary key column "${primaryKeyColumn.columnName}" in the fields array.`);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (!matchingReviewedField.isPrimaryKey) {
|
|
1668
|
+
throw new BadRequestException(`Reviewed metadata JSON must keep datasource primary key column "${primaryKeyColumn.columnName}" marked with isPrimaryKey: true.`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
private buildMissingPrimaryKeyMessage(tableName: string) {
|
|
1674
|
+
return `Table "${tableName}" cannot be mapped because the datasource table does not define any primary key. Add a real primary key to the table before continuing with SolidX mapping.`;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1533
1677
|
private getSuperclassFieldName(columnName: string, legacyTableType: LegacyTableType) {
|
|
1534
1678
|
return this.getLegacySuperclassFieldName(columnName, legacyTableType);
|
|
1535
1679
|
}
|
|
@@ -1771,6 +1915,22 @@ export class DatasourceIntrospectionService {
|
|
|
1771
1915
|
};
|
|
1772
1916
|
}
|
|
1773
1917
|
|
|
1918
|
+
private async writeMigrationFile(migration: MappingPreviewRecord["migration"]) {
|
|
1919
|
+
if (!migration.willGenerate || !migration.content || !migration.filePath) {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
await fs.mkdir(path.dirname(migration.filePath), { recursive: true });
|
|
1924
|
+
await fs.writeFile(migration.filePath, migration.content, "utf8");
|
|
1925
|
+
|
|
1926
|
+
const writtenContent = await fs.readFile(migration.filePath, "utf8").catch(() => null);
|
|
1927
|
+
if (writtenContent !== migration.content) {
|
|
1928
|
+
throw new BadRequestException(`Migration file could not be verified after writing: ${migration.filePath}`);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1774
1934
|
private renderTypeormDatasourceTemplate(input: {
|
|
1775
1935
|
datasource: DatasourceRecord;
|
|
1776
1936
|
entityClassName: string;
|
|
@@ -154,7 +154,7 @@ export class MenuItemMetadataService extends CRUDService<MenuItemMetadata> {
|
|
|
154
154
|
if (!i.parentMenuItem) {
|
|
155
155
|
return true
|
|
156
156
|
} else {
|
|
157
|
-
if(i.action.type === 'custom'){
|
|
157
|
+
if (i.action.type === 'custom') {
|
|
158
158
|
return true;
|
|
159
159
|
}
|
|
160
160
|
return this.crudHelperService.hasReadPermissionOnModel(activeUser, i.action.model.singularName)
|
|
@@ -203,6 +203,7 @@ export class MenuItemMetadataService extends CRUDService<MenuItemMetadata> {
|
|
|
203
203
|
key: rootItem.name.toLowerCase().replace(/\s+/g, '-'),
|
|
204
204
|
icon: rootItem.iconName,
|
|
205
205
|
// iconVariant : rootItem.iconVariant
|
|
206
|
+
isSystem: (rootItem.action?.model?.isSystem ?? false) || rootItem.module?.name === "solid-core",
|
|
206
207
|
}
|
|
207
208
|
if (children.length > 0) {
|
|
208
209
|
data["children"] = await this.buildMenuTree(children, allMenuItems, activeUser);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
2
2
|
import { InjectDataSource } from '@nestjs/typeorm';
|
|
3
3
|
import * as fs from 'fs/promises'; // Use the Promise-based version of fs for async/await
|
|
4
|
+
import { existsSync } from 'fs';
|
|
4
5
|
import * as path from 'path';
|
|
5
6
|
import { DataSource, EntityManager, In, Repository, SelectQueryBuilder } from 'typeorm';
|
|
6
7
|
import { CreateModelMetadataDto } from '../dtos/create-model-metadata.dto';
|
|
@@ -27,7 +28,6 @@ import { PermissionMetadata } from '../entities/permission-metadata.entity';
|
|
|
27
28
|
import { RoleMetadata } from '../entities/role-metadata.entity';
|
|
28
29
|
import { ViewMetadata } from '../entities/view-metadata.entity';
|
|
29
30
|
import { CommandService } from '../helpers/command.service';
|
|
30
|
-
import { startNodemonHeartbeat } from '../helpers/nodemon-heartbeat';
|
|
31
31
|
import {
|
|
32
32
|
REFRESH_MODEL_COMMAND,
|
|
33
33
|
REMOVE_FIELDS_COMMAND,
|
|
@@ -744,10 +744,72 @@ export class ModelMetadataService {
|
|
|
744
744
|
}
|
|
745
745
|
}
|
|
746
746
|
|
|
747
|
+
await this.cleanupAssociatedTypeormDatasourceFiles(modelEntity, modulePath);
|
|
748
|
+
|
|
747
749
|
// - | Drop database table | Removes the database table from the DB, this is a very risky step. Best to review all relations to other models etc and then do this manually | Manual (X)
|
|
748
750
|
|
|
749
751
|
}
|
|
750
752
|
|
|
753
|
+
private resolveSolidApiRoot(): string | null {
|
|
754
|
+
const cwd = process.cwd();
|
|
755
|
+
const candidates = [
|
|
756
|
+
cwd,
|
|
757
|
+
path.join(cwd, 'solid-api'),
|
|
758
|
+
];
|
|
759
|
+
|
|
760
|
+
for (const candidate of candidates) {
|
|
761
|
+
const srcRoot = path.join(candidate, 'src');
|
|
762
|
+
if (existsSync(srcRoot)) {
|
|
763
|
+
return candidate;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private async cleanupAssociatedTypeormDatasourceFiles(modelEntity: ModelMetadata, modulePath?: string | null) {
|
|
771
|
+
if (!modulePath) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const solidApiRoot = this.resolveSolidApiRoot();
|
|
776
|
+
if (!solidApiRoot) {
|
|
777
|
+
this.logger.warn(`Unable to locate consuming solid-api workspace while cleaning datasource files for model '${modelEntity.singularName}'`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const srcRoot = path.join(solidApiRoot, 'src');
|
|
782
|
+
const srcEntries = await fs.readdir(srcRoot).catch(() => []);
|
|
783
|
+
const datasourceFiles = srcEntries
|
|
784
|
+
.filter((entry) => /^typeorm-.*-datasource\.ts$/.test(entry))
|
|
785
|
+
.map((entry) => path.join(srcRoot, entry));
|
|
786
|
+
|
|
787
|
+
if (datasourceFiles.length === 0) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const moduleDirName = path.basename(modulePath);
|
|
792
|
+
const entityClassName = classify(modelEntity.singularName);
|
|
793
|
+
const entityImportPath = `./${moduleDirName}/entities/${kebabCase(modelEntity.singularName)}.entity`;
|
|
794
|
+
|
|
795
|
+
this.logger.log(`Scanning ${datasourceFiles.length} TypeORM datasource file(s) for model '${modelEntity.singularName}' cleanup`);
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
this.solidTsMorphService.begin();
|
|
799
|
+
for (const datasourceFile of datasourceFiles) {
|
|
800
|
+
this.solidTsMorphService.cleanupTypeormDatasourceEntity(
|
|
801
|
+
datasourceFile,
|
|
802
|
+
entityImportPath,
|
|
803
|
+
entityClassName,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
await this.solidTsMorphService.commit();
|
|
807
|
+
} catch (error: any) {
|
|
808
|
+
this.solidTsMorphService.rollback();
|
|
809
|
+
this.logger.error(`Failed to clean datasource files for model '${modelEntity.singularName}':`, error);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
751
813
|
private async cleanupAssociatedViewsActionsAndMenus(modelId: number) {
|
|
752
814
|
const viewRepo = this.dataSource.getRepository(ViewMetadata);
|
|
753
815
|
const actionRepo = this.dataSource.getRepository(ActionMetadata);
|
|
@@ -968,26 +1030,6 @@ export class ModelMetadataService {
|
|
|
968
1030
|
@DisallowInProduction()
|
|
969
1031
|
async generateCodeViaCtl(modelId: number): Promise<string> {
|
|
970
1032
|
const model = await this.findOne(modelId);
|
|
971
|
-
const isEmbedded = process.env.DEFAULT_DATABASE_DRIVER === 'pglite';
|
|
972
|
-
|
|
973
|
-
// When using an embedded PGlite database, the single-connection limit means
|
|
974
|
-
// a spawned `solid refresh-model` subprocess cannot get its own DB connection
|
|
975
|
-
// while the API server is already holding it. Fall back to in-process code
|
|
976
|
-
// generation (which reuses the existing connection).
|
|
977
|
-
if (isEmbedded) {
|
|
978
|
-
this.logger.log('Embedded database detected — generating API code in-process');
|
|
979
|
-
// Prevent nodemon from restarting the server while schematics write files.
|
|
980
|
-
const heartbeat = startNodemonHeartbeat(path.join(process.cwd(), 'src'));
|
|
981
|
-
try {
|
|
982
|
-
return await this.handleGenerateCode({
|
|
983
|
-
modelUserKey: model.singularName,
|
|
984
|
-
dryRun: false,
|
|
985
|
-
});
|
|
986
|
-
} finally {
|
|
987
|
-
heartbeat.stop();
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
1033
|
return this.commandService.executeCommandWithArgs({
|
|
992
1034
|
command: 'npx',
|
|
993
1035
|
args: ['@solidxai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`],
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
} from '../helpers/schematic.service';
|
|
27
27
|
import { CommandService } from '../helpers/command.service';
|
|
28
28
|
import { SolidRegistry } from '../helpers/solid-registry';
|
|
29
|
-
import { startNodemonHeartbeat } from '../helpers/nodemon-heartbeat';
|
|
30
29
|
import { CodeGenerationOptions, ModuleMetadataConfiguration } from '../interfaces';
|
|
31
30
|
import { CrudHelperService } from './crud-helper.service';
|
|
32
31
|
import { ModelMetadataService } from './model-metadata.service';
|
|
@@ -535,40 +534,6 @@ export class ModuleMetadataService {
|
|
|
535
534
|
@DisallowInProduction()
|
|
536
535
|
async generateCodeViaCtl(moduleId: number): Promise<string> {
|
|
537
536
|
const module = await this.findOne(moduleId);
|
|
538
|
-
const isEmbedded = process.env.DEFAULT_DATABASE_DRIVER === 'pglite';
|
|
539
|
-
|
|
540
|
-
// When using an embedded PGlite database, the single-connection limit means
|
|
541
|
-
// a spawned `solid refresh-module` subprocess cannot get its own DB connection
|
|
542
|
-
// while the API server is already holding it. Fall back to in-process code
|
|
543
|
-
// generation (which reuses the existing connection) and only spawn solidctl
|
|
544
|
-
// for the UI module scaffold (which does not need a database connection).
|
|
545
|
-
if (isEmbedded) {
|
|
546
|
-
this.logger.log('Embedded database detected — generating API code in-process');
|
|
547
|
-
// Prevent nodemon from restarting the server while schematics write files.
|
|
548
|
-
// The heartbeat touches a watched file every 1.5 s (nodemon delay is 2 s),
|
|
549
|
-
// keeping the delay from expiring during DB queries between schematic runs.
|
|
550
|
-
const heartbeat = startNodemonHeartbeat(path.join(process.cwd(), 'src'));
|
|
551
|
-
try {
|
|
552
|
-
const apiOutput = await this.generateCode({
|
|
553
|
-
moduleUserKey: module.name,
|
|
554
|
-
dryRun: false,
|
|
555
|
-
});
|
|
556
|
-
let uiOutput = '';
|
|
557
|
-
try {
|
|
558
|
-
uiOutput = await this.commandService.executeCommandWithArgs({
|
|
559
|
-
command: 'npx',
|
|
560
|
-
args: ['@solidxai/solidctl@latest', 'generate', 'ui-module', `--name=${module.name}`],
|
|
561
|
-
cwd: path.join(process.cwd(), '..'),
|
|
562
|
-
});
|
|
563
|
-
} catch (error) {
|
|
564
|
-
this.logger.warn('UI module scaffold failed (solidctl may be outdated); API code was generated successfully.');
|
|
565
|
-
}
|
|
566
|
-
return `${apiOutput}\n${uiOutput}`;
|
|
567
|
-
} finally {
|
|
568
|
-
heartbeat.stop();
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
537
|
return this.commandService.executeCommandWithArgs({
|
|
573
538
|
command: 'npx',
|
|
574
539
|
args: ['@solidxai/solidctl@latest', 'generate', 'module', `--name=${module.name}`],
|