@solidxai/core 0.1.11-beta.9 → 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.
Files changed (114) hide show
  1. package/CHANGELOG.md +456 -0
  2. package/dist/controllers/datasource-introspection.controller.d.ts +8 -0
  3. package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -1
  4. package/dist/controllers/media.controller.js +0 -2
  5. package/dist/controllers/media.controller.js.map +1 -1
  6. package/dist/helpers/environment.helper.d.ts +0 -1
  7. package/dist/helpers/environment.helper.d.ts.map +1 -1
  8. package/dist/helpers/environment.helper.js +0 -4
  9. package/dist/helpers/environment.helper.js.map +1 -1
  10. package/dist/helpers/model-metadata-helper.service.d.ts +1 -2
  11. package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
  12. package/dist/helpers/model-metadata-helper.service.js +2 -6
  13. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  14. package/dist/helpers/module-metadata-helper.service.js +1 -1
  15. package/dist/helpers/module-metadata-helper.service.js.map +1 -1
  16. package/dist/helpers/solid-registry.d.ts +1 -2
  17. package/dist/helpers/solid-registry.d.ts.map +1 -1
  18. package/dist/helpers/solid-registry.js.map +1 -1
  19. package/dist/index.d.ts +0 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +0 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/interfaces.d.ts +1 -0
  24. package/dist/interfaces.d.ts.map +1 -1
  25. package/dist/interfaces.js.map +1 -1
  26. package/dist/repository/scheduled-job.repository.d.ts +2 -2
  27. package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
  28. package/dist/repository/scheduled-job.repository.js +3 -6
  29. package/dist/repository/scheduled-job.repository.js.map +1 -1
  30. package/dist/repository/security-rule.repository.d.ts +2 -2
  31. package/dist/repository/security-rule.repository.d.ts.map +1 -1
  32. package/dist/repository/security-rule.repository.js +3 -6
  33. package/dist/repository/security-rule.repository.js.map +1 -1
  34. package/dist/seeders/seed-data/solid-core-metadata.json +62 -33
  35. package/dist/services/authentication.service.d.ts +2 -0
  36. package/dist/services/authentication.service.d.ts.map +1 -1
  37. package/dist/services/authentication.service.js +32 -11
  38. package/dist/services/authentication.service.js.map +1 -1
  39. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
  40. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +5 -7
  41. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
  42. package/dist/services/crud-helper.service.d.ts.map +1 -1
  43. package/dist/services/crud-helper.service.js +7 -0
  44. package/dist/services/crud-helper.service.js.map +1 -1
  45. package/dist/services/datasource-introspection.service.d.ts +10 -0
  46. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  47. package/dist/services/datasource-introspection.service.js +109 -17
  48. package/dist/services/datasource-introspection.service.js.map +1 -1
  49. package/dist/services/menu-item-metadata.service.d.ts.map +1 -1
  50. package/dist/services/menu-item-metadata.service.js +1 -0
  51. package/dist/services/menu-item-metadata.service.js.map +1 -1
  52. package/dist/services/model-metadata.service.d.ts +2 -0
  53. package/dist/services/model-metadata.service.d.ts.map +1 -1
  54. package/dist/services/model-metadata.service.js +49 -16
  55. package/dist/services/model-metadata.service.js.map +1 -1
  56. package/dist/services/module-metadata.service.d.ts.map +1 -1
  57. package/dist/services/module-metadata.service.js +0 -28
  58. package/dist/services/module-metadata.service.js.map +1 -1
  59. package/dist/services/settings/default-settings-provider.service.d.ts +212 -0
  60. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  61. package/dist/services/settings/default-settings-provider.service.js +106 -0
  62. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  63. package/dist/services/solid-ts-morph.service.d.ts +6 -0
  64. package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
  65. package/dist/services/solid-ts-morph.service.js +43 -0
  66. package/dist/services/solid-ts-morph.service.js.map +1 -1
  67. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  68. package/dist/subscribers/audit.subscriber.js +2 -8
  69. package/dist/subscribers/audit.subscriber.js.map +1 -1
  70. package/dist/subscribers/computed-entity-field.subscriber.d.ts +0 -8
  71. package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
  72. package/dist/subscribers/computed-entity-field.subscriber.js +3 -36
  73. package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
  74. package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
  75. package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
  76. package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
  77. package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
  78. package/dist/subscribers/scheduled-job.subscriber.d.ts.map +1 -1
  79. package/dist/subscribers/scheduled-job.subscriber.js +2 -3
  80. package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
  81. package/dist/subscribers/security-rule.subscriber.d.ts.map +1 -1
  82. package/dist/subscribers/security-rule.subscriber.js +3 -5
  83. package/dist/subscribers/security-rule.subscriber.js.map +1 -1
  84. package/package.json +1 -1
  85. package/src/controllers/media.controller.ts +1 -1
  86. package/src/helpers/environment.helper.ts +0 -11
  87. package/src/helpers/model-metadata-helper.service.ts +2 -7
  88. package/src/helpers/module-metadata-helper.service.ts +1 -1
  89. package/src/helpers/solid-registry.ts +1 -7
  90. package/src/index.ts +0 -1
  91. package/src/interfaces.ts +1 -0
  92. package/src/repository/scheduled-job.repository.ts +4 -10
  93. package/src/repository/security-rule.repository.ts +4 -10
  94. package/src/seeders/seed-data/solid-core-metadata.json +62 -33
  95. package/src/services/authentication.service.ts +46 -12
  96. package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +5 -11
  97. package/src/services/crud-helper.service.ts +7 -0
  98. package/src/services/datasource-introspection.service.ts +176 -16
  99. package/src/services/menu-item-metadata.service.ts +2 -1
  100. package/src/services/model-metadata.service.ts +63 -22
  101. package/src/services/module-metadata.service.ts +0 -36
  102. package/src/services/settings/default-settings-provider.service.ts +106 -0
  103. package/src/services/solid-ts-morph.service.ts +61 -1
  104. package/src/subscribers/audit.subscriber.ts +2 -11
  105. package/src/subscribers/computed-entity-field.subscriber.ts +6 -63
  106. package/src/subscribers/created-by-updated-by.subscriber.ts +0 -3
  107. package/src/subscribers/field-metadata.subscriber.ts +0 -3
  108. package/src/subscribers/scheduled-job.subscriber.ts +2 -3
  109. package/src/subscribers/security-rule.subscriber.ts +3 -8
  110. package/dist/helpers/nodemon-heartbeat.d.ts +0 -5
  111. package/dist/helpers/nodemon-heartbeat.d.ts.map +0 -1
  112. package/dist/helpers/nodemon-heartbeat.js +0 -67
  113. package/dist/helpers/nodemon-heartbeat.js.map +0 -1
  114. package/src/helpers/nodemon-heartbeat.ts +0 -59
@@ -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
- if (plan.migration.willGenerate && plan.migration.content && plan.migration.filePath) {
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: Boolean(plan.migration.willGenerate && plan.migration.filePath),
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, inventory.columnsByKey.get(tableKey) ?? []);
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 === reviewedModel.userKeyFieldUserKey,
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: mappingDto.userKeyField,
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 userKeyFieldUserKey = readString("userKeyFieldUserKey");
892
- if (!normalizedFields.some((field) => field.name === userKeyFieldUserKey && !field.isMarkedForRemoval)) {
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 === userKeyFieldUserKey,
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,8 +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 { isEmbeddedDb } from '../helpers/environment.helper';
31
- import { startNodemonHeartbeat } from '../helpers/nodemon-heartbeat';
32
31
  import {
33
32
  REFRESH_MODEL_COMMAND,
34
33
  REMOVE_FIELDS_COMMAND,
@@ -745,10 +744,72 @@ export class ModelMetadataService {
745
744
  }
746
745
  }
747
746
 
747
+ await this.cleanupAssociatedTypeormDatasourceFiles(modelEntity, modulePath);
748
+
748
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)
749
750
 
750
751
  }
751
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
+
752
813
  private async cleanupAssociatedViewsActionsAndMenus(modelId: number) {
753
814
  const viewRepo = this.dataSource.getRepository(ViewMetadata);
754
815
  const actionRepo = this.dataSource.getRepository(ActionMetadata);
@@ -969,26 +1030,6 @@ export class ModelMetadataService {
969
1030
  @DisallowInProduction()
970
1031
  async generateCodeViaCtl(modelId: number): Promise<string> {
971
1032
  const model = await this.findOne(modelId);
972
- const isEmbedded = isEmbeddedDb();
973
-
974
- // When using an embedded PGlite database, the single-connection limit means
975
- // a spawned `solid refresh-model` subprocess cannot get its own DB connection
976
- // while the API server is already holding it. Fall back to in-process code
977
- // generation (which reuses the existing connection).
978
- if (isEmbedded) {
979
- this.logger.log('Embedded database detected — generating API code in-process');
980
- // Prevent nodemon from restarting the server while schematics write files.
981
- const heartbeat = startNodemonHeartbeat(path.join(process.cwd(), 'src'));
982
- try {
983
- return await this.handleGenerateCode({
984
- modelUserKey: model.singularName,
985
- dryRun: false,
986
- });
987
- } finally {
988
- heartbeat.stop();
989
- }
990
- }
991
-
992
1033
  return this.commandService.executeCommandWithArgs({
993
1034
  command: 'npx',
994
1035
  args: ['@solidxai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`],
@@ -26,8 +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 { isEmbeddedDb } from '../helpers/environment.helper';
30
- import { startNodemonHeartbeat } from '../helpers/nodemon-heartbeat';
31
29
  import { CodeGenerationOptions, ModuleMetadataConfiguration } from '../interfaces';
32
30
  import { CrudHelperService } from './crud-helper.service';
33
31
  import { ModelMetadataService } from './model-metadata.service';
@@ -536,40 +534,6 @@ export class ModuleMetadataService {
536
534
  @DisallowInProduction()
537
535
  async generateCodeViaCtl(moduleId: number): Promise<string> {
538
536
  const module = await this.findOne(moduleId);
539
- const isEmbedded = isEmbeddedDb();
540
-
541
- // When using an embedded PGlite database, the single-connection limit means
542
- // a spawned `solid refresh-module` subprocess cannot get its own DB connection
543
- // while the API server is already holding it. Fall back to in-process code
544
- // generation (which reuses the existing connection) and only spawn solidctl
545
- // for the UI module scaffold (which does not need a database connection).
546
- if (isEmbedded) {
547
- this.logger.log('Embedded database detected — generating API code in-process');
548
- // Prevent nodemon from restarting the server while schematics write files.
549
- // The heartbeat touches a watched file every 1.5 s (nodemon delay is 2 s),
550
- // keeping the delay from expiring during DB queries between schematic runs.
551
- const heartbeat = startNodemonHeartbeat(path.join(process.cwd(), 'src'));
552
- try {
553
- const apiOutput = await this.generateCode({
554
- moduleUserKey: module.name,
555
- dryRun: false,
556
- });
557
- let uiOutput = '';
558
- try {
559
- uiOutput = await this.commandService.executeCommandWithArgs({
560
- command: 'npx',
561
- args: ['@solidxai/solidctl@latest', 'generate', 'ui-module', `--name=${module.name}`],
562
- cwd: path.join(process.cwd(), '..'),
563
- });
564
- } catch (error) {
565
- this.logger.warn('UI module scaffold failed (solidctl may be outdated); API code was generated successfully.');
566
- }
567
- return `${apiOutput}\n${uiOutput}`;
568
- } finally {
569
- heartbeat.stop();
570
- }
571
- }
572
-
573
537
  return this.commandService.executeCommandWithArgs({
574
538
  command: 'npx',
575
539
  args: ['@solidxai/solidctl@latest', 'generate', 'module', `--name=${module.name}`],