@solidxai/core 0.1.11-beta.4 → 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 (104) 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/dtos/datasource-introspection-mapping.dto.d.ts +1 -0
  6. package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -1
  7. package/dist/dtos/datasource-introspection-mapping.dto.js +7 -1
  8. package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -1
  9. package/dist/helpers/command.service.d.ts +2 -0
  10. package/dist/helpers/command.service.d.ts.map +1 -1
  11. package/dist/helpers/command.service.js +6 -2
  12. package/dist/helpers/command.service.js.map +1 -1
  13. package/dist/helpers/nodemon-heartbeat.d.ts +5 -0
  14. package/dist/helpers/nodemon-heartbeat.d.ts.map +1 -0
  15. package/dist/helpers/nodemon-heartbeat.js +67 -0
  16. package/dist/helpers/nodemon-heartbeat.js.map +1 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/seeders/module-metadata-seeder.service.d.ts +7 -1
  22. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  23. package/dist/seeders/module-metadata-seeder.service.js +101 -1
  24. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  25. package/dist/seeders/seed-data/solid-core-metadata.json +109 -31
  26. package/dist/services/api-key.service.d.ts.map +1 -1
  27. package/dist/services/api-key.service.js.map +1 -1
  28. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +4 -0
  29. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -1
  30. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +18 -1
  31. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -1
  32. package/dist/services/crud.service.d.ts +1 -0
  33. package/dist/services/crud.service.d.ts.map +1 -1
  34. package/dist/services/crud.service.js +27 -0
  35. package/dist/services/crud.service.js.map +1 -1
  36. package/dist/services/datasource-introspection.service.d.ts +1 -0
  37. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  38. package/dist/services/datasource-introspection.service.js +27 -16
  39. package/dist/services/datasource-introspection.service.js.map +1 -1
  40. package/dist/services/import-transaction.service.d.ts.map +1 -1
  41. package/dist/services/import-transaction.service.js +3 -0
  42. package/dist/services/import-transaction.service.js.map +1 -1
  43. package/dist/services/model-metadata.service.d.ts +1 -0
  44. package/dist/services/model-metadata.service.d.ts.map +1 -1
  45. package/dist/services/model-metadata.service.js +28 -0
  46. package/dist/services/model-metadata.service.js.map +1 -1
  47. package/dist/services/module-metadata.service.d.ts.map +1 -1
  48. package/dist/services/module-metadata.service.js +27 -0
  49. package/dist/services/module-metadata.service.js.map +1 -1
  50. package/dist/services/removed-field-migration.service.d.ts +30 -0
  51. package/dist/services/removed-field-migration.service.d.ts.map +1 -0
  52. package/dist/services/removed-field-migration.service.js +319 -0
  53. package/dist/services/removed-field-migration.service.js.map +1 -0
  54. package/dist/solid-core.module.d.ts.map +1 -1
  55. package/dist/solid-core.module.js +4 -0
  56. package/dist/solid-core.module.js.map +1 -1
  57. package/dist/subscribers/audit.subscriber.d.ts +4 -1
  58. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  59. package/dist/subscribers/audit.subscriber.js +54 -6
  60. package/dist/subscribers/audit.subscriber.js.map +1 -1
  61. package/dist-tests/api/authenticate.spec.js +119 -0
  62. package/dist-tests/api/authenticate.spec.js.map +1 -0
  63. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
  64. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
  65. package/dist-tests/api/ping.spec.js +21 -0
  66. package/dist-tests/api/ping.spec.js.map +1 -0
  67. package/dist-tests/helpers/auth.js +41 -0
  68. package/dist-tests/helpers/auth.js.map +1 -0
  69. package/dist-tests/helpers/env.js +11 -0
  70. package/dist-tests/helpers/env.js.map +1 -0
  71. package/docs/agent-hub-grooming.md +301 -0
  72. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
  73. package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
  74. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
  75. package/docs/datasource-introspection-ddl-analysis.md +326 -0
  76. package/docs/datasource-introspection-implementation-plan.md +306 -0
  77. package/docs/grouping-enhancements.md +89 -0
  78. package/docs/java-spring/README.md +3 -0
  79. package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
  80. package/docs/module-package-import-handoff.md +691 -0
  81. package/docs/seed-changes.md +65 -0
  82. package/docs/test-data-workflow.md +200 -0
  83. package/docs/type-declaration-import-issue.md +24 -0
  84. package/package.json +1 -1
  85. package/src/commands/migrate-removed-fields.command.ts +81 -0
  86. package/src/dtos/datasource-introspection-mapping.dto.ts +5 -0
  87. package/src/helpers/command.service.ts +10 -2
  88. package/src/helpers/nodemon-heartbeat.ts +59 -0
  89. package/src/index.ts +1 -0
  90. package/src/seeders/module-metadata-seeder.service.ts +118 -0
  91. package/src/seeders/seed-data/solid-core-metadata.json +109 -31
  92. package/src/services/api-key.service.ts +36 -36
  93. package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +15 -1
  94. package/src/services/crud.service.ts +36 -11
  95. package/src/services/datasource-introspection.service.ts +29 -16
  96. package/src/services/import-transaction.service.ts +5 -0
  97. package/src/services/model-metadata.service.ts +45 -1
  98. package/src/services/module-metadata.service.ts +35 -0
  99. package/src/services/removed-field-migration.service.ts +334 -0
  100. package/src/solid-core.module.ts +4 -0
  101. package/src/subscribers/audit.subscriber.ts +70 -5
  102. package/.claude/settings.local.json +0 -16
  103. package/CLAUDE.md +0 -26
  104. package/src/services/1.js +0 -6
@@ -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,16 +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
- "Bash(node verify-imports.js)"
11
- ],
12
- "additionalDirectories": [
13
- "/Users/oswald/projects/Solid_Starters/solidctl"
14
- ]
15
- }
16
- }
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
package/src/services/1.js DELETED
@@ -1,6 +0,0 @@
1
- 1. Do i need to create a storeStreams method for aws service too?
2
- - Handle later
3
- 2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
4
- - startExport should either return the data or return the transaction id
5
- 3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
6
- - show the userKey