@solidxai/core 0.1.11-beta.2 → 0.1.11-beta.5

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 (145) hide show
  1. package/dist/commands/migrate-removed-fields.command.d.ts +19 -0
  2. package/dist/commands/migrate-removed-fields.command.d.ts.map +1 -0
  3. package/dist/commands/migrate-removed-fields.command.js +77 -0
  4. package/dist/commands/migrate-removed-fields.command.js.map +1 -0
  5. package/dist/controllers/datasource-introspection.controller.d.ts +38 -1
  6. package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -1
  7. package/dist/controllers/datasource-introspection.controller.js +38 -0
  8. package/dist/controllers/datasource-introspection.controller.js.map +1 -1
  9. package/dist/dtos/datasource-introspection-mapping.dto.d.ts +5 -0
  10. package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -1
  11. package/dist/dtos/datasource-introspection-mapping.dto.js +24 -2
  12. package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -1
  13. package/dist/entities/{legacy-common-with-id.entity.d.ts → legacy-common-with-generated-id.entity.d.ts} +2 -2
  14. package/dist/entities/legacy-common-with-generated-id.entity.d.ts.map +1 -0
  15. package/dist/entities/{legacy-common-with-id.entity.js → legacy-common-with-generated-id.entity.js} +4 -4
  16. package/dist/entities/legacy-common-with-generated-id.entity.js.map +1 -0
  17. package/dist/entities/{legacy-common.entity.d.ts → legacy-common.entity-with-existing-id.entity.d.ts} +1 -1
  18. package/dist/entities/legacy-common.entity-with-existing-id.entity.d.ts.map +1 -0
  19. package/dist/entities/{legacy-common.entity.js → legacy-common.entity-with-existing-id.entity.js} +1 -1
  20. package/dist/entities/legacy-common.entity-with-existing-id.entity.js.map +1 -0
  21. package/dist/helpers/command.service.d.ts +2 -0
  22. package/dist/helpers/command.service.d.ts.map +1 -1
  23. package/dist/helpers/command.service.js +6 -2
  24. package/dist/helpers/command.service.js.map +1 -1
  25. package/dist/helpers/model-metadata-helper.service.js +3 -3
  26. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  27. package/dist/helpers/nodemon-heartbeat.d.ts +5 -0
  28. package/dist/helpers/nodemon-heartbeat.d.ts.map +1 -0
  29. package/dist/helpers/nodemon-heartbeat.js +67 -0
  30. package/dist/helpers/nodemon-heartbeat.js.map +1 -0
  31. package/dist/index.d.ts +3 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/interfaces/datasource-introspection-provider.interface.d.ts +1 -0
  36. package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -1
  37. package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -1
  38. package/dist/seeders/module-metadata-seeder.service.d.ts +7 -1
  39. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  40. package/dist/seeders/module-metadata-seeder.service.js +101 -1
  41. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  42. package/dist/seeders/seed-data/solid-core-metadata.json +121 -34
  43. package/dist/services/api-key.service.d.ts.map +1 -1
  44. package/dist/services/api-key.service.js.map +1 -1
  45. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +4 -0
  46. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -1
  47. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +18 -1
  48. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -1
  49. package/dist/services/crud.service.d.ts +1 -0
  50. package/dist/services/crud.service.d.ts.map +1 -1
  51. package/dist/services/crud.service.js +27 -0
  52. package/dist/services/crud.service.js.map +1 -1
  53. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -1
  54. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +12 -0
  55. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -1
  56. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -1
  57. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +2 -0
  58. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -1
  59. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -1
  60. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +28 -15
  61. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -1
  62. package/dist/services/datasource-introspection.service.d.ts +53 -2
  63. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  64. package/dist/services/datasource-introspection.service.js +559 -174
  65. package/dist/services/datasource-introspection.service.js.map +1 -1
  66. package/dist/services/import-transaction.service.d.ts.map +1 -1
  67. package/dist/services/import-transaction.service.js +3 -0
  68. package/dist/services/import-transaction.service.js.map +1 -1
  69. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +10 -10
  70. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  71. package/dist/services/model-metadata.service.d.ts +1 -0
  72. package/dist/services/model-metadata.service.d.ts.map +1 -1
  73. package/dist/services/model-metadata.service.js +28 -0
  74. package/dist/services/model-metadata.service.js.map +1 -1
  75. package/dist/services/module-metadata.service.d.ts.map +1 -1
  76. package/dist/services/module-metadata.service.js +27 -0
  77. package/dist/services/module-metadata.service.js.map +1 -1
  78. package/dist/services/removed-field-migration.service.d.ts +30 -0
  79. package/dist/services/removed-field-migration.service.d.ts.map +1 -0
  80. package/dist/services/removed-field-migration.service.js +319 -0
  81. package/dist/services/removed-field-migration.service.js.map +1 -0
  82. package/dist/solid-core.module.d.ts.map +1 -1
  83. package/dist/solid-core.module.js +4 -0
  84. package/dist/solid-core.module.js.map +1 -1
  85. package/dist/subscribers/audit.subscriber.d.ts +4 -1
  86. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  87. package/dist/subscribers/audit.subscriber.js +54 -6
  88. package/dist/subscribers/audit.subscriber.js.map +1 -1
  89. package/dist-tests/api/authenticate.spec.js +119 -0
  90. package/dist-tests/api/authenticate.spec.js.map +1 -0
  91. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
  92. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
  93. package/dist-tests/api/ping.spec.js +21 -0
  94. package/dist-tests/api/ping.spec.js.map +1 -0
  95. package/dist-tests/helpers/auth.js +41 -0
  96. package/dist-tests/helpers/auth.js.map +1 -0
  97. package/dist-tests/helpers/env.js +11 -0
  98. package/dist-tests/helpers/env.js.map +1 -0
  99. package/docs/agent-hub-grooming.md +301 -0
  100. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
  101. package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
  102. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
  103. package/docs/datasource-introspection-ddl-analysis.md +326 -0
  104. package/docs/datasource-introspection-implementation-plan.md +306 -0
  105. package/docs/grouping-enhancements.md +89 -0
  106. package/docs/java-spring/README.md +3 -0
  107. package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
  108. package/docs/module-package-import-handoff.md +691 -0
  109. package/docs/seed-changes.md +65 -0
  110. package/docs/test-data-workflow.md +200 -0
  111. package/docs/type-declaration-import-issue.md +24 -0
  112. package/package.json +1 -1
  113. package/src/commands/migrate-removed-fields.command.ts +81 -0
  114. package/src/controllers/datasource-introspection.controller.ts +27 -1
  115. package/src/dtos/datasource-introspection-mapping.dto.ts +15 -0
  116. package/src/entities/{legacy-common-with-id.entity.ts → legacy-common-with-generated-id.entity.ts} +1 -1
  117. package/src/helpers/command.service.ts +10 -2
  118. package/src/helpers/model-metadata-helper.service.ts +1 -1
  119. package/src/helpers/nodemon-heartbeat.ts +59 -0
  120. package/src/index.ts +3 -2
  121. package/src/interfaces/datasource-introspection-provider.interface.ts +1 -0
  122. package/src/seeders/module-metadata-seeder.service.ts +118 -0
  123. package/src/seeders/seed-data/solid-core-metadata.json +121 -34
  124. package/src/services/api-key.service.ts +36 -36
  125. package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +15 -1
  126. package/src/services/crud.service.ts +36 -11
  127. package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +12 -0
  128. package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +2 -0
  129. package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +28 -15
  130. package/src/services/datasource-introspection.service.ts +653 -150
  131. package/src/services/import-transaction.service.ts +5 -0
  132. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +2 -2
  133. package/src/services/model-metadata.service.ts +45 -1
  134. package/src/services/module-metadata.service.ts +35 -0
  135. package/src/services/removed-field-migration.service.ts +334 -0
  136. package/src/solid-core.module.ts +4 -0
  137. package/src/subscribers/audit.subscriber.ts +70 -5
  138. package/.claude/settings.local.json +0 -15
  139. package/CLAUDE.md +0 -26
  140. package/dist/entities/legacy-common-with-id.entity.d.ts.map +0 -1
  141. package/dist/entities/legacy-common-with-id.entity.js.map +0 -1
  142. package/dist/entities/legacy-common.entity.d.ts.map +0 -1
  143. package/dist/entities/legacy-common.entity.js.map +0 -1
  144. package/src/services/1.js +0 -6
  145. /package/src/entities/{legacy-common.entity.ts → legacy-common.entity-with-existing-id.entity.ts} +0 -0
@@ -204,6 +204,11 @@ export class ImportTransactionService extends CRUDService<ImportTransaction> {
204
204
 
205
205
  // Iterate through the fields and populate the standard instructions
206
206
  for (const field of allFields) {
207
+
208
+ // Skip Computed fields
209
+ if (field.type === SolidFieldType.computed) {
210
+ continue;
211
+ }
207
212
  // Skip system fields
208
213
  if (systemFieldNames.includes(field.name)) {
209
214
  continue;
@@ -2,8 +2,8 @@ import { Injectable, Logger } from "@nestjs/common";
2
2
  import { ConfigService } from "@nestjs/config";
3
3
  import { CommonEntity } from "src/entities/common.entity";
4
4
  import { FieldMetadata } from "src/entities/field-metadata.entity";
5
- import { LegacyCommonEntityWithExistingId } from "src/entities/legacy-common.entity";
6
- import { LegacyCommonEntityWithGeneratedId } from "src/entities/legacy-common-with-id.entity";
5
+ import { LegacyCommonEntityWithExistingId } from "src/entities/legacy-common.entity-with-existing-id.entity";
6
+ import { LegacyCommonEntityWithGeneratedId } from "src/entities/legacy-common-with-generated-id.entity";
7
7
  import { Media } from "src/entities/media.entity";
8
8
  import { MediaStorageProvider } from "src/interfaces";
9
9
  import { DiskFileService, S3FileService } from "src/services/file";
@@ -27,6 +27,7 @@ import { PermissionMetadata } from '../entities/permission-metadata.entity';
27
27
  import { RoleMetadata } from '../entities/role-metadata.entity';
28
28
  import { ViewMetadata } from '../entities/view-metadata.entity';
29
29
  import { CommandService } from '../helpers/command.service';
30
+ import { startNodemonHeartbeat } from '../helpers/nodemon-heartbeat';
30
31
  import {
31
32
  REFRESH_MODEL_COMMAND,
32
33
  REMOVE_FIELDS_COMMAND,
@@ -967,6 +968,26 @@ export class ModelMetadataService {
967
968
  @DisallowInProduction()
968
969
  async generateCodeViaCtl(modelId: number): Promise<string> {
969
970
  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
+
970
991
  return this.commandService.executeCommandWithArgs({
971
992
  command: 'npx',
972
993
  args: ['@solidxai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`],
@@ -1478,7 +1499,7 @@ export class ModelMetadataService {
1478
1499
  //Filter out the fields by id
1479
1500
  const fieldsForRemoval = model.fields.filter((field) => options.fieldIdsForRemoval.includes(+field.id));
1480
1501
  const removeOutput = await this.executeRemoveFieldsCommand(model, fieldsForRemoval, options.dryRun);
1481
-
1502
+ const migrationOutput = await this.executeMigrationRemoveFieldsCommand(model, fieldsForRemoval, options.dryRun);
1482
1503
  // Remove the fields from the database as well. This also checks, if the field is marked for removal
1483
1504
  for (const field of fieldsForRemoval) {
1484
1505
  if (field.isMarkedForRemoval) {
@@ -1515,6 +1536,13 @@ export class ModelMetadataService {
1515
1536
 
1516
1537
  return removeOutput;
1517
1538
  }
1539
+ // model-metadata.service.ts mein naya method add karo
1540
+ // async executeRemoveFieldsWithModel(model: ModelMetadata, fieldNames: string[], dryRun = false,) {
1541
+ // const fieldsForRemoval = model.fields.filter(
1542
+ // field => fieldNames.includes(field.name)
1543
+ // );
1544
+ // return this.executeRemoveFieldsCommand(model, fieldsForRemoval, dryRun);
1545
+ // }
1518
1546
 
1519
1547
  async generateModelCode(options: CodeGenerationOptions): Promise<string> {
1520
1548
  if (!options.modelId && !options.modelUserKey) {
@@ -1563,6 +1591,22 @@ export class ModelMetadataService {
1563
1591
  return output;
1564
1592
  }
1565
1593
 
1594
+ private async executeMigrationRemoveFieldsCommand(model: ModelMetadata, fieldsForRemoval: FieldMetadata[], dryRun: boolean = false,): Promise<string> {
1595
+ if (!fieldsForRemoval || fieldsForRemoval.length === 0) {
1596
+ return "";
1597
+ }
1598
+
1599
+ const output = await this.commandService.executeCommandWithArgs({
1600
+ command: 'npx',
1601
+ args: ['@solidxai/solidctl@latest', 'migration', '-n', model.singularName, 'remove-field', ...(dryRun ? [] : ['--apply'])],
1602
+ cwd: path.join(process.cwd(), '..'),
1603
+ });
1604
+
1605
+ this.logger.debug(`Schematic output : ${output}`);
1606
+ return output;
1607
+ }
1608
+
1609
+
1566
1610
  async updateUserKey(data: any) {
1567
1611
  const { modelName, fieldName } = data;
1568
1612
 
@@ -26,6 +26,7 @@ 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';
29
30
  import { CodeGenerationOptions, ModuleMetadataConfiguration } from '../interfaces';
30
31
  import { CrudHelperService } from './crud-helper.service';
31
32
  import { ModelMetadataService } from './model-metadata.service';
@@ -534,6 +535,40 @@ export class ModuleMetadataService {
534
535
  @DisallowInProduction()
535
536
  async generateCodeViaCtl(moduleId: number): Promise<string> {
536
537
  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
+
537
572
  return this.commandService.executeCommandWithArgs({
538
573
  command: 'npx',
539
574
  args: ['@solidxai/solidctl@latest', 'generate', 'module', `--name=${module.name}`],
@@ -0,0 +1,334 @@
1
+ import * as fs from "fs/promises";
2
+ import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
3
+ import { ModuleRef } from "@nestjs/core";
4
+ import { getDataSourceToken } from "@nestjs/typeorm";
5
+ import { kebabCase, snakeCase } from "lodash";
6
+ import * as path from "path";
7
+ import { ERROR_MESSAGES } from "src/constants/error-messages";
8
+ import { RelationType, SolidFieldType } from "src/dtos/create-field-metadata.dto";
9
+ import { FieldMetadata } from "src/entities/field-metadata.entity";
10
+ import { ModelMetadata } from "src/entities/model-metadata.entity";
11
+ import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service";
12
+ import { classify } from "src/helpers/string.helper";
13
+ import { FieldMetadataRepository } from "src/repository/field-metadata.repository";
14
+ import { ModelMetadataRepository } from "src/repository/model-metadata.repository";
15
+ import { DataSource, EntityMetadata, QueryRunner, Table } from "typeorm";
16
+
17
+ export interface RemovedFieldMigrationResult {
18
+ dryRun: boolean;
19
+ modelName: string;
20
+ operations: string[];
21
+ removedFieldNames: string[];
22
+ }
23
+
24
+ @Injectable()
25
+ export class RemovedFieldMigrationService {
26
+ constructor(
27
+ private readonly modelMetadataRepo: ModelMetadataRepository,
28
+ private readonly fieldMetadataRepo: FieldMetadataRepository,
29
+ private readonly moduleMetadataHelperService: ModuleMetadataHelperService,
30
+ private readonly moduleRef: ModuleRef,
31
+ ) { }
32
+
33
+ private readonly logger = new Logger(RemovedFieldMigrationService.name);
34
+
35
+ // Cleans fields marked for removal by updating schema state and metadata for a single model.
36
+ async migrateMarkedFields(modelUserKey: string, dryRun: boolean = false): Promise<RemovedFieldMigrationResult> {
37
+ if (!modelUserKey) {
38
+ throw new BadRequestException("Model name is required");
39
+ }
40
+
41
+ const model = await this.modelMetadataRepo.findOne({
42
+ where: { singularName: modelUserKey },
43
+ relations: { fields: true, module: true },
44
+ });
45
+
46
+ if (!model) {
47
+ throw new NotFoundException(ERROR_MESSAGES.MODEL_NOT_FOUND(modelUserKey));
48
+ }
49
+
50
+ const fieldsForRemoval = model.fields.filter((field) => field.isMarkedForRemoval);
51
+ const operations: string[] = [];
52
+
53
+ if (fieldsForRemoval.length === 0) {
54
+ const message = `No fields marked for removal were found for model "${model.singularName}".`;
55
+ this.logger.log(message);
56
+ operations.push(message);
57
+ return {
58
+ dryRun,
59
+ modelName: model.singularName,
60
+ operations,
61
+ removedFieldNames: [],
62
+ };
63
+ }
64
+
65
+ const dataSource = await this.resolveDataSource(model.dataSource);
66
+ const entityMetadata = this.resolveEntityMetadata(dataSource, model);
67
+ const queryRunner = dataSource.createQueryRunner();
68
+
69
+ try {
70
+ await queryRunner.connect();
71
+
72
+ if (!dryRun) {
73
+ await queryRunner.startTransaction();
74
+ }
75
+
76
+ for (const field of fieldsForRemoval) {
77
+ await this.cleanupMarkedField({
78
+ field,
79
+ model,
80
+ entityMetadata,
81
+ queryRunner,
82
+ dryRun,
83
+ operations,
84
+ });
85
+ }
86
+
87
+ if (!dryRun) {
88
+ await queryRunner.commitTransaction();
89
+ }
90
+ } catch (error) {
91
+ if (!dryRun && queryRunner.isTransactionActive) {
92
+ await queryRunner.rollbackTransaction();
93
+ }
94
+ throw error;
95
+ } finally {
96
+ await queryRunner.release();
97
+ }
98
+
99
+ return {
100
+ dryRun,
101
+ modelName: model.singularName,
102
+ operations,
103
+ removedFieldNames: fieldsForRemoval.map((field) => field.name),
104
+ };
105
+ }
106
+
107
+ private async cleanupMarkedField(params: { field: FieldMetadata; model: ModelMetadata; entityMetadata?: EntityMetadata; queryRunner: QueryRunner; dryRun: boolean; operations: string[]; }): Promise<void> {
108
+ const { field, model, entityMetadata, queryRunner, dryRun, operations } = params;
109
+ const relationMetadata = entityMetadata?.relations.find((relation) => relation.propertyName === field.name);
110
+ const resolvedTableName = entityMetadata?.tableName || model.tableName;
111
+
112
+ if (field.type !== SolidFieldType.relation) {
113
+ const columnCandidates = this.buildColumnCandidates(field, entityMetadata, relationMetadata);
114
+ await this.dropColumnsForField(resolvedTableName, field, columnCandidates, queryRunner, dryRun, operations);
115
+ return;
116
+ }
117
+
118
+ if (field.relationType === RelationType.manyToOne) {
119
+ const columnCandidates = this.buildColumnCandidates(field, entityMetadata, relationMetadata);
120
+ await this.dropColumnsForField(resolvedTableName, field, columnCandidates, queryRunner, dryRun, operations);
121
+ return;
122
+ }
123
+
124
+ if (field.relationType === RelationType.manyTomany) {
125
+ const joinTableName = relationMetadata?.junctionEntityMetadata?.tableName || field.relationJoinTableName;
126
+ const ownsJoinTable = relationMetadata?.isOwning || field.isRelationManyToManyOwner;
127
+
128
+ if (!ownsJoinTable) {
129
+ operations.push(`No direct database cleanup required for inverse many-to-many field "${field.name}".`);
130
+ return;
131
+ }
132
+
133
+ if (!joinTableName) {
134
+ operations.push(`Skipping join-table cleanup for "${field.name}" because no join table name could be resolved.`);
135
+ return;
136
+ }
137
+
138
+ await this.dropJoinTable(joinTableName, field, queryRunner, dryRun, operations);
139
+ return;
140
+ }
141
+
142
+ operations.push(`No direct database cleanup required for relation field "${field.name}" with type "${field.relationType}".`);
143
+ }
144
+
145
+ private async dropColumnsForField(tableName: string, field: FieldMetadata, columnCandidates: string[], queryRunner: QueryRunner, dryRun: boolean, operations: string[],): Promise<void> {
146
+ if (!tableName) {
147
+ operations.push(`Skipping field "${field.name}" because the model table name could not be resolved.`);
148
+ return;
149
+ }
150
+
151
+ if (columnCandidates.length === 0) {
152
+ operations.push(`Skipping field "${field.name}" because no column candidates could be resolved.`);
153
+ return;
154
+ }
155
+
156
+ const handledColumns = new Set<string>();
157
+ let droppedAnyColumn = false;
158
+
159
+ for (const columnName of columnCandidates) {
160
+ if (!columnName || handledColumns.has(columnName)) {
161
+ continue;
162
+ }
163
+
164
+ handledColumns.add(columnName);
165
+ const table = await this.loadTable(queryRunner, tableName);
166
+ const column = table?.columns.find((tableColumn) => tableColumn.name === columnName);
167
+
168
+ if (!column) {
169
+ continue;
170
+ }
171
+
172
+ droppedAnyColumn = true;
173
+ await this.dropColumnArtifacts(table, columnName, queryRunner, dryRun, operations);
174
+ }
175
+
176
+ if (!droppedAnyColumn) {
177
+ operations.push(`No database column found for field "${field.name}" on table "${tableName}". Metadata cleanup will still proceed.`);
178
+ }
179
+ }
180
+
181
+ private async dropColumnArtifacts(table: Table, columnName: string, queryRunner: QueryRunner, dryRun: boolean, operations: string[],): Promise<void> {
182
+ for (const foreignKey of table.foreignKeys.filter((item) => item.columnNames.includes(columnName))) {
183
+ operations.push(`Drop foreign key "${foreignKey.name}" on "${table.name}.${columnName}"`);
184
+ if (!dryRun) {
185
+ await queryRunner.dropForeignKey(table, foreignKey);
186
+ }
187
+ }
188
+
189
+ for (const uniqueConstraint of table.uniques.filter((item) => item.columnNames.includes(columnName))) {
190
+ operations.push(`Drop unique constraint "${uniqueConstraint.name}" on "${table.name}.${columnName}"`);
191
+ if (!dryRun) {
192
+ await queryRunner.dropUniqueConstraint(table, uniqueConstraint);
193
+ }
194
+ }
195
+
196
+ for (const index of table.indices.filter((item) => item.columnNames.includes(columnName))) {
197
+ operations.push(`Drop index "${index.name}" on "${table.name}.${columnName}"`);
198
+ if (!dryRun) {
199
+ await queryRunner.dropIndex(table, index);
200
+ }
201
+ }
202
+
203
+ operations.push(`Drop column "${table.name}.${columnName}"`);
204
+ if (!dryRun) {
205
+ await queryRunner.dropColumn(table, columnName);
206
+ }
207
+ }
208
+
209
+ private async dropJoinTable(joinTableName: string, field: FieldMetadata, queryRunner: QueryRunner, dryRun: boolean, operations: string[]): Promise<void> {
210
+ const table = await this.loadTable(queryRunner, joinTableName);
211
+ if (!table) {
212
+ operations.push(`Join table "${joinTableName}" for field "${field.name}" does not exist. Metadata cleanup will still proceed.`);
213
+ return;
214
+ }
215
+
216
+ operations.push(`Drop join table "${joinTableName}" for field "${field.name}"`);
217
+ if (!dryRun) {
218
+ await queryRunner.dropTable(joinTableName);
219
+ }
220
+ }
221
+
222
+ private buildColumnCandidates(field: FieldMetadata, entityMetadata?: EntityMetadata, relationMetadata?: EntityMetadata["relations"][number]): string[] {
223
+ const columnCandidates = new Set<string>();
224
+
225
+ relationMetadata?.joinColumns?.forEach((column) => {
226
+ if (column.databaseName) {
227
+ columnCandidates.add(column.databaseName);
228
+ }
229
+ });
230
+
231
+ entityMetadata?.columns
232
+ .filter((column) => column.propertyName === field.name)
233
+ .forEach((column) => {
234
+ if (column.databaseName) {
235
+ columnCandidates.add(column.databaseName);
236
+ }
237
+ });
238
+
239
+ if (field.columnName) {
240
+ columnCandidates.add(field.columnName);
241
+ }
242
+
243
+ if (field.relationCoModelColumnName) {
244
+ columnCandidates.add(field.relationCoModelColumnName);
245
+ }
246
+
247
+ if (field.type === SolidFieldType.relation && field.relationType === RelationType.manyToOne) {
248
+ columnCandidates.add(`${snakeCase(field.name)}_id`);
249
+ } else {
250
+ columnCandidates.add(snakeCase(field.name));
251
+ }
252
+
253
+ return [...columnCandidates].filter(Boolean);
254
+ }
255
+
256
+ private async resolveDataSource(dataSourceName?: string): Promise<DataSource> {
257
+ const normalizedDataSourceName = dataSourceName && dataSourceName !== "default" ? dataSourceName : undefined;
258
+ const token = normalizedDataSourceName ? getDataSourceToken(normalizedDataSourceName) : getDataSourceToken();
259
+ let dataSource: DataSource | undefined;
260
+
261
+ try {
262
+ dataSource = this.moduleRef.get<DataSource>(token, { strict: false });
263
+ } catch (error: any) {
264
+ throw new NotFoundException(`Datasource "${normalizedDataSourceName ?? "default"}" could not be resolved: ${error?.message ?? error}`);
265
+ }
266
+
267
+ if (!dataSource) {
268
+ throw new NotFoundException(`Datasource "${normalizedDataSourceName ?? "default"}" could not be resolved.`);
269
+ }
270
+
271
+ if (!dataSource.isInitialized) {
272
+ await dataSource.initialize();
273
+ }
274
+
275
+ return dataSource;
276
+ }
277
+
278
+ private resolveEntityMetadata(dataSource: DataSource, model: ModelMetadata): EntityMetadata | undefined {
279
+ const candidates = [classify(model.singularName), model.singularName, model.tableName].filter(Boolean);
280
+
281
+ for (const candidate of candidates) {
282
+ try {
283
+ return dataSource.getMetadata(candidate);
284
+ } catch {
285
+ // Try the next candidate.
286
+ }
287
+ }
288
+
289
+ return dataSource.entityMetadatas.find((metadata) => metadata.tableName === model.tableName);
290
+ }
291
+
292
+ private async resolveMetadataFilePath(moduleName: string): Promise<string> {
293
+ const defaultPath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
294
+ if (await this.fileExists(defaultPath)) {
295
+ return defaultPath;
296
+ }
297
+
298
+ const dashModuleName = kebabCase(moduleName);
299
+ const moduleMetadataFilePath = path.resolve(
300
+ process.cwd(),
301
+ "module-metadata",
302
+ dashModuleName,
303
+ `${dashModuleName}-metadata.json`,
304
+ );
305
+
306
+ if (await this.fileExists(moduleMetadataFilePath)) {
307
+ return moduleMetadataFilePath;
308
+ }
309
+
310
+ return defaultPath;
311
+ }
312
+
313
+ private async fileExists(filePath: string): Promise<boolean> {
314
+ try {
315
+ await fs.access(filePath);
316
+ return true;
317
+ } catch {
318
+ return false;
319
+ }
320
+ }
321
+
322
+ private async loadTable(queryRunner: QueryRunner, tableName: string): Promise<Table | undefined> {
323
+ if (!tableName) {
324
+ return undefined;
325
+ }
326
+
327
+ const hasTable = await queryRunner.hasTable(tableName);
328
+ if (!hasTable) {
329
+ return undefined;
330
+ }
331
+
332
+ return queryRunner.getTable(tableName) ?? undefined;
333
+ }
334
+ }
@@ -13,6 +13,7 @@ import {
13
13
  import { MulterModule } from "@nestjs/platform-express";
14
14
  import { TypeOrmModule } from "@nestjs/typeorm";
15
15
  import { RemoveFieldsCommand } from "./commands/remove-fields.command";
16
+ import { MigrateRemovedFieldsCommand } from "./commands/migrate-removed-fields.command";
16
17
  import { FieldMetadataController } from "./controllers/field-metadata.controller";
17
18
  import { DashboardController } from "./controllers/dashboard.controller";
18
19
  import { MediaStorageProviderMetadataController } from "./controllers/media-storage-provider-metadata.controller";
@@ -47,6 +48,7 @@ import { ListOfValuesService } from "./services/list-of-values.service";
47
48
  import { MediaStorageProviderMetadataService } from "./services/media-storage-provider-metadata.service";
48
49
  import { MediaService } from "./services/media.service";
49
50
  import { ModelMetadataService } from "./services/model-metadata.service";
51
+ import { RemovedFieldMigrationService } from "./services/removed-field-migration.service";
50
52
  import { ModuleMetadataExplorerService } from "./services/module-metadata-explorer.service";
51
53
  import { ModuleMetadataService } from "./services/module-metadata.service";
52
54
  import { ModulePackageService } from "./services/module-package.service";
@@ -545,6 +547,7 @@ import { PostgresDatasourceIntrospectionProviderService } from "./services/datas
545
547
  FieldMetadataService,
546
548
  DashboardRuntimeService,
547
549
  RemoveFieldsCommand,
550
+ MigrateRemovedFieldsCommand,
548
551
  RefreshModelCommand,
549
552
  RefreshModuleCommand,
550
553
  InfoCommand,
@@ -791,6 +794,7 @@ import { PostgresDatasourceIntrospectionProviderService } from "./services/datas
791
794
  UserViewMetadataRepository,
792
795
  ModelMetadataRepository,
793
796
  ModuleMetadataRepository,
797
+ RemovedFieldMigrationService,
794
798
  ActionMetadataRepository,
795
799
  MediaStorageProviderMetadataRepository,
796
800
  FixturesService,
@@ -1,12 +1,14 @@
1
1
  import { Injectable, Logger, Scope } from '@nestjs/common';
2
+ import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
2
3
  import { lowerFirst } from 'src/helpers/string.helper';
3
4
  import { SolidRegistry } from 'src/helpers/solid-registry';
4
5
  import { DataSource, EntityMetadata, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
5
6
  import { AuditQueuePayload } from 'src/interfaces';
6
7
  import { RequestContextService } from 'src/services/request-context.service';
7
8
  import { PublisherFactory } from 'src/services/queues/publisher-factory.service';
9
+ const AUDIT_BEFORE_SNAPSHOT = '__auditBeforeSnapshot';
8
10
 
9
- @Injectable({scope: Scope.TRANSIENT})
11
+ @Injectable({ scope: Scope.TRANSIENT })
10
12
  export class AuditSubscriber implements EntitySubscriberInterface {
11
13
  private readonly logger = new Logger(AuditSubscriber.name);
12
14
  private dataSource: DataSource;
@@ -14,6 +16,7 @@ export class AuditSubscriber implements EntitySubscriberInterface {
14
16
  private readonly publisherFactory: PublisherFactory<AuditQueuePayload>,
15
17
  private readonly solidRegistry: SolidRegistry,
16
18
  private readonly requestContextService: RequestContextService,
19
+ private readonly modelMetadataHelperService: ModelMetadataHelperService,
17
20
  ) { }
18
21
 
19
22
  bindToDataSource(dataSource: DataSource) {
@@ -53,20 +56,82 @@ export class AuditSubscriber implements EntitySubscriberInterface {
53
56
 
54
57
  async afterUpdate(event: UpdateEvent<any>) {
55
58
  if (!this.shouldTrackAudit(event.metadata)) return;
59
+ const entityId = event.entity?.id ?? event.databaseEntity?.id ?? (event as any).entityId ?? null;
60
+ let before = event.databaseEntity ?? null;
61
+ let after = event.entity ?? null;
62
+ const updatedColumnNames = (event.updatedColumns ?? []).map(c => c.propertyName);
63
+
64
+ const auditResult = await this.prepareManyToManyAuditUpdateSnapshot(event, entityId, updatedColumnNames);
65
+
66
+ if (auditResult) {
67
+ before = auditResult.before;
68
+ after = auditResult.after;
69
+ }
70
+
56
71
  this.enqueue(event, {
57
72
  eventType: 'update',
58
73
  modelName: event.metadata.name,
59
- entityId: event.entity?.id ?? null,
74
+ entityId: entityId,
60
75
  occurredAt: new Date().toISOString(),
61
- after: event.entity ?? null,
76
+ after: after,
62
77
  // databaseEntity is only populated when the entity was fetched first (save() path).
63
78
  // QueryBuilder update() leaves this undefined; postAuditMessageOnUpdate guards for it.
64
- before: event.databaseEntity ?? null,
65
- updatedColumnNames: (event.updatedColumns ?? []).map(c => c.propertyName),
79
+ before: before,
80
+ updatedColumnNames: updatedColumnNames,
66
81
  userId: this.activeUserId(),
67
82
  });
68
83
  }
69
84
 
85
+ /**
86
+ * Resolves before/after snapshots for audit-enabled many-to-many relations
87
+ * and updates changed relation field names for audit tracking.
88
+ */
89
+ private async prepareManyToManyAuditUpdateSnapshot(event: UpdateEvent<any>, entityId: number | null, updatedColumnNames: string[]): Promise<{ before: any; after: any } | null> {
90
+
91
+ const auditRelationFields = (await this.modelMetadataHelperService.loadFieldHierarchy(lowerFirst(event.metadata.name))).filter(field =>
92
+ field.enableAuditTracking &&
93
+ field.type === 'relation' &&
94
+ field.relationType !== 'one-to-many'
95
+ );
96
+
97
+ if (!entityId || auditRelationFields.length === 0) {
98
+ return null;
99
+ }
100
+
101
+ const relations: Record<string, boolean> = {};
102
+
103
+ auditRelationFields.forEach(field => {
104
+ relations[field.name] = true;
105
+ });
106
+
107
+ const relationBefore = event.entity?.[AUDIT_BEFORE_SNAPSHOT] ?? null;
108
+
109
+ const relationAfter = await event.manager.getRepository(event.metadata.target as any).findOne({
110
+ where: { id: entityId } as any,
111
+ relations: relations as any,
112
+ });
113
+
114
+ if (relationBefore && relationAfter) {
115
+ auditRelationFields.forEach(field => {
116
+ const oldIds = Array.isArray(relationBefore[field.name])
117
+ ? relationBefore[field.name].map(item => item.id).sort()
118
+ : [];
119
+
120
+ const newIds = Array.isArray(relationAfter[field.name])
121
+ ? relationAfter[field.name].map(item => item.id).sort()
122
+ : [];
123
+
124
+ if ((oldIds.length !== newIds.length || JSON.stringify(oldIds) !== JSON.stringify(newIds))
125
+ && !updatedColumnNames.includes(field.name)) {
126
+ updatedColumnNames.push(field.name);
127
+ }
128
+ });
129
+ }
130
+ return {
131
+ before: relationBefore ?? event.databaseEntity ?? null,
132
+ after: relationAfter ?? event.entity ?? null,
133
+ };
134
+ }
70
135
  async afterRemove(event: RemoveEvent<any>) {
71
136
  if (!this.shouldTrackAudit(event.metadata)) return;
72
137
  this.enqueue(event, {
@@ -1,15 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(node -e:*)",
5
- "WebFetch(domain:docs.solidxai.com)",
6
- "WebFetch(domain:github.com)",
7
- "Read(//Users/oswald/projects/Solid_Starters/solidctl/**)",
8
- "Read(//Users/oswald/projects/Solid_Starters/**)",
9
- "Bash(find /Users/oswald/projects/Solid_Starters/solid-core-module -type d -name migration* -o -name *database*)"
10
- ],
11
- "additionalDirectories": [
12
- "/Users/oswald/projects/Solid_Starters/solidctl"
13
- ]
14
- }
15
- }
package/CLAUDE.md DELETED
@@ -1,26 +0,0 @@
1
- # This is the SolidX API project.
2
- This is included as a dependency in consuming projects.
3
-
4
- # SolidX UI
5
- # The corresponding UI project is at the below path:
6
- /Users/oswald/projects/Solid_Starters/solid-core-ui
7
-
8
- # SolidX Docs
9
- # The documentation project is at the below path:
10
- /Users/oswald/projects/solidxai-docs
11
-
12
- # SolidX Agent
13
- # The agent project which has the skills for helping users configure, build and use SolidX is at the below path:
14
- /Users/oswald/projects/Solid_Starters/agent
15
-
16
- # SolidX Control Plane Commands
17
- # The control plane commands for managing SolidX instances are in the below path:
18
- /Users/oswald/projects/Solid_Starters/solidctl
19
-
20
- # Testing reference project
21
- # This is the reference library management project that is used for testing and development of SolidX features. It is located at the below path:
22
- /Users/oswald/projects/Solid_Starters/solid-library-management
23
-
24
- # Code Builder project
25
- # This is the project that contains the code builder which generates code based on user input and templates. It is located at the below path:
26
- /Users/oswald/projects/Solid_Starters/solid-code-builder
@@ -1 +0,0 @@
1
- {"version":3,"file":"legacy-common-with-id.entity.d.ts","sourceRoot":"","sources":["../../src/entities/legacy-common-with-id.entity.ts"],"names":[],"mappings":"AAEA,OAAO,EAA8B,gCAAgC,EAAE,MAAM,wBAAwB,CAAC;AAEtG,8BACsB,iCAAkC,SAAQ,gCAAgC;IAI5F,EAAE,EAAE,MAAM,CAAA;CACb"}