@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
@@ -4,12 +4,14 @@ import { getDataSourceToken } from "@nestjs/typeorm";
4
4
  import { createHash } from "crypto";
5
5
  import * as fs from "fs/promises";
6
6
  import * as path from "path";
7
- import { camelCase, startCase } from "lodash";
7
+ import { camelCase, kebabCase, startCase } from "lodash";
8
+ import { ArrayLiteralExpression, IndentationText, Node, ObjectLiteralExpression, Project, QuoteKind, SyntaxKind, VariableDeclarationKind } from "ts-morph";
8
9
  import { DataSource } from "typeorm";
9
10
  import { CreateModelMetadataDto } from "src/dtos/create-model-metadata.dto";
10
11
  import { DatasourceIntrospectionMappingColumnDto, DatasourceIntrospectionMappingDto } from "src/dtos/datasource-introspection-mapping.dto";
11
12
  import { UpdateModelMetaDataDto } from "src/dtos/update-model-metadata.dto";
12
13
  import { LegacyTableType } from "src/enums/legacy-table-type.enum";
14
+ import { classify } from "src/helpers/string.helper";
13
15
  import type { DatasourceIntrospectionColumn, DatasourceIntrospectionProvider, DatasourceIntrospectionTableRef } from "src/interfaces/datasource-introspection-provider.interface";
14
16
  import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service";
15
17
  import { ModelMetadataHelperService } from "src/helpers/model-metadata-helper.service";
@@ -17,8 +19,10 @@ import { ModelMetadataRepository } from "src/repository/model-metadata.repositor
17
19
  import { ModuleMetadataRepository } from "src/repository/module-metadata.repository";
18
20
  import type { ModelMetadata } from "src/entities/model-metadata.entity";
19
21
  import { DatasourceManagementService, type DatasourceRecord } from "./datasource-management.service";
22
+ import { CommandService } from "src/helpers/command.service";
20
23
  import { FieldMetadataService } from "./field-metadata.service";
21
24
  import { ModelMetadataService } from "./model-metadata.service";
25
+ import { ModuleMetadataService } from "./module-metadata.service";
22
26
  import { MssqlDatasourceIntrospectionProviderService } from "./datasource-introspection/mssql-datasource-introspection-provider.service";
23
27
  import { MysqlDatasourceIntrospectionProviderService } from "./datasource-introspection/mysql-datasource-introspection-provider.service";
24
28
  import { PostgresDatasourceIntrospectionProviderService } from "./datasource-introspection/postgres-datasource-introspection-provider.service";
@@ -44,6 +48,7 @@ type TableInventoryRecord = {
44
48
  type TableDetailColumnRecord = {
45
49
  columnName: string;
46
50
  fieldName: string;
51
+ displayName: string;
47
52
  dataType: string;
48
53
  solidFieldType: string;
49
54
  ormType: string;
@@ -54,6 +59,7 @@ type TableDetailColumnRecord = {
54
59
  numericScale: number | null;
55
60
  ordinalPosition: number | null;
56
61
  isAutoGenerated: boolean;
62
+ isPrimaryKey: boolean;
57
63
  handledBySuperclass: boolean;
58
64
  superclassFieldName: string | null;
59
65
  };
@@ -81,6 +87,13 @@ type MappingPreviewRecord = {
81
87
  note: string;
82
88
  missingSystemColumns: string[];
83
89
  };
90
+ datasourceFile: {
91
+ filePath: string;
92
+ created: boolean;
93
+ updated: boolean;
94
+ entityRegistered: boolean;
95
+ migrationGlobRegistered: boolean;
96
+ };
84
97
  summary: {
85
98
  includedColumnNames: string[];
86
99
  removedColumnNames: string[];
@@ -119,6 +132,105 @@ type SystemColumnDefinition = {
119
132
  isUnique?: boolean;
120
133
  };
121
134
 
135
+ type DatasourceTypeFamily =
136
+ | "boolean"
137
+ | "bigint"
138
+ | "int"
139
+ | "numeric"
140
+ | "decimal"
141
+ | "float"
142
+ | "date"
143
+ | "time"
144
+ | "datetime"
145
+ | "datetimeWithTimezone"
146
+ | "json"
147
+ | "jsonb"
148
+ | "uuid"
149
+ | "longText"
150
+ | "mediumText"
151
+ | "shortText"
152
+ | "unicodeShortText";
153
+
154
+ const DATASOURCE_TYPE_FAMILY_ALIASES: Record<DatasourceTypeFamily, string[]> = {
155
+ boolean: ["bit", "bool", "boolean"],
156
+ bigint: ["bigint"],
157
+ int: ["int", "integer", "smallint", "tinyint"],
158
+ numeric: ["numeric"],
159
+ decimal: ["decimal", "money", "smallmoney"],
160
+ float: ["float", "double", "double precision", "real", "float4", "float8"],
161
+ date: ["date"],
162
+ time: ["time", "time without time zone", "time with time zone"],
163
+ datetime: ["datetime", "datetime2", "smalldatetime", "timestamp", "timestamp without time zone"],
164
+ datetimeWithTimezone: ["timestamptz", "timestamp with time zone"],
165
+ json: ["json", "simple-json", "simplejson"],
166
+ jsonb: ["jsonb"],
167
+ uuid: ["uniqueidentifier", "uuid"],
168
+ longText: ["text", "ntext", "longtext", "clob"],
169
+ mediumText: ["mediumtext"],
170
+ shortText: ["varchar", "char", "citext"],
171
+ unicodeShortText: ["nvarchar", "nchar"],
172
+ };
173
+
174
+ const ORM_TYPE_BY_DATASOURCE_FAMILY: Record<string, Partial<Record<DatasourceTypeFamily, string>>> = {
175
+ mssql: {
176
+ boolean: "bit",
177
+ bigint: "bigint",
178
+ int: "int",
179
+ numeric: "numeric",
180
+ decimal: "decimal",
181
+ float: "decimal",
182
+ date: "date",
183
+ time: "time",
184
+ datetime: "datetime",
185
+ datetimeWithTimezone: "datetime",
186
+ json: "simple-json",
187
+ jsonb: "simple-json",
188
+ uuid: "uniqueidentifier",
189
+ longText: "text",
190
+ mediumText: "text",
191
+ shortText: "varchar",
192
+ unicodeShortText: "nvarchar",
193
+ },
194
+ mysql: {
195
+ boolean: "boolean",
196
+ bigint: "int",
197
+ int: "int",
198
+ numeric: "decimal",
199
+ decimal: "decimal",
200
+ float: "double",
201
+ date: "date",
202
+ time: "time",
203
+ datetime: "datetime",
204
+ datetimeWithTimezone: "datetime",
205
+ json: "json",
206
+ jsonb: "json",
207
+ uuid: "varchar",
208
+ longText: "longtext",
209
+ mediumText: "mediumtext",
210
+ shortText: "varchar",
211
+ unicodeShortText: "varchar",
212
+ },
213
+ postgres: {
214
+ boolean: "boolean",
215
+ bigint: "bigint",
216
+ int: "integer",
217
+ numeric: "decimal",
218
+ decimal: "decimal",
219
+ float: "decimal",
220
+ date: "date",
221
+ time: "time",
222
+ datetime: "timestamp",
223
+ datetimeWithTimezone: "timestamptz",
224
+ json: "json",
225
+ jsonb: "jsonb",
226
+ uuid: "uuid",
227
+ longText: "text",
228
+ mediumText: "text",
229
+ shortText: "varchar",
230
+ unicodeShortText: "varchar",
231
+ },
232
+ };
233
+
122
234
  @Injectable()
123
235
  export class DatasourceIntrospectionService {
124
236
  private readonly supportedDatasourceTypes = new Set(["mssql", "mysql", "postgres"]);
@@ -132,6 +244,8 @@ export class DatasourceIntrospectionService {
132
244
  private readonly modelMetadataHelperService: ModelMetadataHelperService,
133
245
  private readonly fieldMetadataService: FieldMetadataService,
134
246
  private readonly modelMetadataService: ModelMetadataService,
247
+ private readonly moduleMetadataService: ModuleMetadataService,
248
+ private readonly commandService: CommandService,
135
249
  private readonly mssqlProvider: MssqlDatasourceIntrospectionProviderService,
136
250
  private readonly mysqlProvider: MysqlDatasourceIntrospectionProviderService,
137
251
  private readonly postgresProvider: PostgresDatasourceIntrospectionProviderService,
@@ -266,27 +380,116 @@ export class DatasourceIntrospectionService {
266
380
  ? await this.modelMetadataService.update(mappingDto.modelId, plan.modelPayload as UpdateModelMetaDataDto)
267
381
  : await this.modelMetadataService.create(plan.modelPayload as CreateModelMetadataDto);
268
382
 
383
+ const persistedModelId = mappingDto.modelId ?? (persistedModel as ModelMetadata | undefined)?.id ?? null;
384
+
385
+ return {
386
+ data: {
387
+ modelId: persistedModelId,
388
+ operation: plan.operation,
389
+ metadataJson: plan.metadataJson,
390
+ summary: plan.summary,
391
+ },
392
+ };
393
+ }
394
+
395
+ async createMigrationArtifacts(moduleId: number, mappingDto: DatasourceIntrospectionMappingDto) {
396
+ const plan = await this.buildMappingPreview(moduleId, mappingDto);
397
+ const module = await this.loadModule(moduleId);
398
+ const datasource = this.requireDatasource(mappingDto.dataSource);
399
+
400
+ if (!mappingDto.modelId) {
401
+ throw new BadRequestException("Persist the model mapping first so the generated entity can be registered before creating migration artifacts.");
402
+ }
403
+
269
404
  if (plan.migration.willGenerate && plan.migration.content && plan.migration.filePath) {
270
405
  await fs.mkdir(path.dirname(plan.migration.filePath), { recursive: true });
271
406
  await fs.writeFile(plan.migration.filePath, plan.migration.content, "utf8");
272
407
  }
273
408
 
274
- const persistedModelId = mappingDto.modelId ?? (persistedModel as ModelMetadata | undefined)?.id ?? null;
409
+ const datasourceFile = await this.syncTypeormDatasourceFile(
410
+ module.name,
411
+ datasource,
412
+ plan.modelPayload.singularName,
413
+ );
275
414
 
276
415
  return {
277
416
  data: {
278
- modelId: persistedModelId,
417
+ modelId: mappingDto.modelId,
279
418
  operation: plan.operation,
280
- metadataJson: plan.metadataJson,
281
419
  migration: {
282
420
  ...plan.migration,
283
421
  written: Boolean(plan.migration.willGenerate && plan.migration.filePath),
284
422
  },
423
+ datasourceFile,
285
424
  summary: plan.summary,
286
425
  },
287
426
  };
288
427
  }
289
428
 
429
+ async generateCode(moduleId: number) {
430
+ await this.loadModule(moduleId);
431
+ const output = await this.moduleMetadataService.generateCodeViaCtl(moduleId);
432
+
433
+ return {
434
+ data: {
435
+ moduleId,
436
+ output,
437
+ message: "Module code generation completed.",
438
+ },
439
+ };
440
+ }
441
+
442
+ async runMigration(moduleId: number, datasourceName: string) {
443
+ const module = await this.loadModule(moduleId);
444
+ const datasource = this.requireDatasource(datasourceName);
445
+ const cwd = path.join(process.cwd(), "..");
446
+ const outputs: string[] = [];
447
+
448
+ const migrationOutput = await this.commandService.executeCommandWithArgs({
449
+ command: "npx",
450
+ args: ["-y", "@solidxai/solidctl@latest", "migration", "-d", datasource.name, "run"],
451
+ cwd,
452
+ });
453
+ outputs.push([
454
+ `npx @solidxai/solidctl@latest migration -d ${datasource.name} run`,
455
+ migrationOutput,
456
+ ].filter(Boolean).join("\n"));
457
+
458
+ const buildOutput = await this.commandService.executeCommandWithArgs({
459
+ command: "npx",
460
+ args: ["-y", "@solidxai/solidctl@latest", "build"],
461
+ cwd,
462
+ });
463
+ outputs.push([
464
+ "npx @solidxai/solidctl@latest build",
465
+ buildOutput,
466
+ ].filter(Boolean).join("\n"));
467
+
468
+ const seedOutput = await this.commandService.executeCommandWithArgs({
469
+ command: "npx",
470
+ args: ["-y", "@solidxai/solidctl@latest", "seed", "--modules-to-seed", module.name],
471
+ cwd,
472
+ });
473
+ outputs.push([
474
+ `npx @solidxai/solidctl@latest seed --modules-to-seed ${module.name}`,
475
+ seedOutput,
476
+ ].filter(Boolean).join("\n"));
477
+
478
+ return {
479
+ data: {
480
+ moduleId,
481
+ datasource: datasource.name,
482
+ output: outputs.join("\n\n"),
483
+ command: [
484
+ `npx @solidxai/solidctl@latest migration -d ${datasource.name} run`,
485
+ "npx @solidxai/solidctl@latest build",
486
+ `npx @solidxai/solidctl@latest seed --modules-to-seed ${module.name}`,
487
+ ].join("\n"),
488
+ message: `Migration, build, and seed completed successfully for datasource "${datasource.name}".`,
489
+ },
490
+ };
491
+ }
492
+
290
493
  private async buildMappingPreview(moduleId: number, mappingDto: DatasourceIntrospectionMappingDto): Promise<MappingPreviewRecord> {
291
494
  const module = await this.loadModule(moduleId);
292
495
  const datasource = this.requireDatasource(mappingDto.dataSource);
@@ -338,6 +541,13 @@ export class DatasourceIntrospectionService {
338
541
  mappedModelId: mappingDto.modelId ?? null,
339
542
  metadataJson,
340
543
  migration,
544
+ datasourceFile: {
545
+ filePath: path.resolve(process.cwd(), "src", `typeorm-${datasource.name}-datasource.ts`),
546
+ created: false,
547
+ updated: false,
548
+ entityRegistered: false,
549
+ migrationGlobRegistered: false,
550
+ },
341
551
  summary: {
342
552
  includedColumnNames,
343
553
  removedColumnNames,
@@ -608,6 +818,14 @@ export class DatasourceIntrospectionService {
608
818
  return rest;
609
819
  }
610
820
 
821
+ private sanitizeConfiguredFieldPayload(fieldConfig: Record<string, any> | undefined) {
822
+ if (!fieldConfig || typeof fieldConfig !== "object" || Array.isArray(fieldConfig)) {
823
+ return {};
824
+ }
825
+
826
+ return this.sanitizeExistingFieldPayload(fieldConfig);
827
+ }
828
+
611
829
  private normalizeReviewedMetadataModel(
612
830
  reviewedModel: Record<string, any> | undefined,
613
831
  mappingDto: DatasourceIntrospectionMappingDto,
@@ -762,34 +980,39 @@ export class DatasourceIntrospectionService {
762
980
  existingField?: any,
763
981
  ) {
764
982
  const preserved = existingField ? this.sanitizeExistingFieldPayload(existingField) : {};
983
+ const configured = this.sanitizeConfiguredFieldPayload(column.fieldConfig);
984
+ const merged = {
985
+ ...preserved,
986
+ ...configured,
987
+ };
765
988
  const maxLength =
766
989
  column.characterMaximumLength && ["shortText", "longText"].includes(column.solidFieldType)
767
990
  ? column.characterMaximumLength
768
- : preserved.max;
991
+ : merged.max;
769
992
 
770
993
  return {
771
- ...preserved,
994
+ ...merged,
772
995
  ...(column.fieldId ? { id: column.fieldId } : {}),
773
996
  name: column.fieldName,
774
- displayName: preserved.displayName ?? startCase(column.fieldName),
775
- description: preserved.description ?? "",
997
+ displayName: column.displayName?.trim() || merged.displayName || startCase(column.fieldName),
998
+ description: merged.description ?? "",
776
999
  type: column.solidFieldType,
777
1000
  ormType: column.ormType,
778
- length: preserved.length ?? column.characterMaximumLength ?? undefined,
779
- defaultValue: preserved.defaultValue ?? column.defaultValue ?? undefined,
780
- required: preserved.required ?? (!column.nullable || column.isPrimaryKey),
781
- unique: preserved.unique ?? false,
782
- encrypt: preserved.encrypt ?? false,
783
- index: preserved.index ?? false,
1001
+ length: merged.length ?? column.characterMaximumLength ?? undefined,
1002
+ defaultValue: merged.defaultValue ?? column.defaultValue ?? undefined,
1003
+ required: merged.required ?? (!column.nullable || column.isPrimaryKey),
1004
+ unique: merged.unique ?? false,
1005
+ encrypt: merged.encrypt ?? false,
1006
+ index: merged.index ?? false,
784
1007
  max: maxLength ?? undefined,
785
- min: preserved.min ?? undefined,
786
- private: preserved.private ?? false,
787
- isSystem: preserved.isSystem ?? false,
1008
+ min: merged.min ?? undefined,
1009
+ private: merged.private ?? false,
1010
+ isSystem: merged.isSystem ?? false,
788
1011
  isMarkedForRemoval: false,
789
1012
  columnName: column.columnName,
790
1013
  isUserKey: mappingDto.userKeyField === column.fieldName,
791
- enableAuditTracking: preserved.enableAuditTracking ?? mappingDto.enableAuditTracking,
792
- isMultiSelect: preserved.isMultiSelect ?? false,
1014
+ enableAuditTracking: merged.enableAuditTracking ?? mappingDto.enableAuditTracking,
1015
+ isMultiSelect: merged.isMultiSelect ?? false,
793
1016
  isPrimaryKey: column.isPrimaryKey,
794
1017
  };
795
1018
  }
@@ -891,11 +1114,8 @@ export class DatasourceIntrospectionService {
891
1114
 
892
1115
  const upColumnStatements = missingSystemColumns.map((definition) => {
893
1116
  const columnLiteral = this.renderTableColumnLiteral(definition);
894
- return [
895
- ` if (!(await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)}))) {`,
896
- ` await queryRunner.addColumn(tableName, new TableColumn(${columnLiteral}));`,
897
- " }",
898
- ].join("\n");
1117
+ return ` if (!(await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)})))\n`
1118
+ + ` await queryRunner.addColumn(tableName, new TableColumn(${columnLiteral}));`;
899
1119
  }).join("\n\n");
900
1120
 
901
1121
  const postUpStatements: string[] = [];
@@ -904,12 +1124,11 @@ export class DatasourceIntrospectionService {
904
1124
  [
905
1125
  " {",
906
1126
  " const table = await queryRunner.getTable(tableName);",
907
- ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(deletedAtIndexName)})) {`,
908
- " await queryRunner.createIndex(tableName, new TableIndex({",
909
- ` name: ${JSON.stringify(deletedAtIndexName)},`,
910
- " columnNames: [\"ss_deleted_at\"],",
911
- " }));",
912
- " }",
1127
+ ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(deletedAtIndexName)}))`,
1128
+ ` await queryRunner.createIndex(tableName, new TableIndex(${this.renderTableIndexLiteral({
1129
+ name: deletedAtIndexName,
1130
+ columnNames: ["ss_deleted_at"],
1131
+ })}));`,
913
1132
  " }",
914
1133
  ].join("\n"),
915
1134
  );
@@ -919,13 +1138,12 @@ export class DatasourceIntrospectionService {
919
1138
  [
920
1139
  " {",
921
1140
  " const table = await queryRunner.getTable(tableName);",
922
- ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(generatedIdUniqueName)})) {`,
923
- " await queryRunner.createIndex(tableName, new TableIndex({",
924
- ` name: ${JSON.stringify(generatedIdUniqueName)},`,
925
- " columnNames: [\"ss_id\"],",
926
- " isUnique: true,",
927
- " }));",
928
- " }",
1141
+ ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(generatedIdUniqueName)}))`,
1142
+ ` await queryRunner.createIndex(tableName, new TableIndex(${this.renderTableIndexLiteral({
1143
+ name: generatedIdUniqueName,
1144
+ columnNames: ["ss_id"],
1145
+ isUnique: true,
1146
+ })}));`,
929
1147
  " }",
930
1148
  ].join("\n"),
931
1149
  );
@@ -938,9 +1156,8 @@ export class DatasourceIntrospectionService {
938
1156
  " {",
939
1157
  " const table = await queryRunner.getTable(tableName);",
940
1158
  ` const index = table?.indices.find((entry) => entry.name === ${JSON.stringify(generatedIdUniqueName)});`,
941
- " if (index) {",
1159
+ " if (index)",
942
1160
  " await queryRunner.dropIndex(tableName, index);",
943
- " }",
944
1161
  " }",
945
1162
  ].join("\n"),
946
1163
  );
@@ -951,9 +1168,8 @@ export class DatasourceIntrospectionService {
951
1168
  " {",
952
1169
  " const table = await queryRunner.getTable(tableName);",
953
1170
  ` const index = table?.indices.find((entry) => entry.name === ${JSON.stringify(deletedAtIndexName)});`,
954
- " if (index) {",
1171
+ " if (index)",
955
1172
  " await queryRunner.dropIndex(tableName, index);",
956
- " }",
957
1173
  " }",
958
1174
  ].join("\n"),
959
1175
  );
@@ -962,11 +1178,8 @@ export class DatasourceIntrospectionService {
962
1178
  ...missingSystemColumns
963
1179
  .slice()
964
1180
  .reverse()
965
- .map((definition) => [
966
- ` if (await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)})) {`,
967
- ` await queryRunner.dropColumn(tableName, ${JSON.stringify(definition.columnName)});`,
968
- " }",
969
- ].join("\n")),
1181
+ .map((definition) => ` if (await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)}))\n`
1182
+ + ` await queryRunner.dropColumn(tableName, ${JSON.stringify(definition.columnName)});`),
970
1183
  );
971
1184
 
972
1185
  return [
@@ -992,29 +1205,41 @@ export class DatasourceIntrospectionService {
992
1205
 
993
1206
  private renderTableColumnLiteral(definition: SystemColumnDefinition) {
994
1207
  const properties: string[] = [
995
- `{\n name: ${JSON.stringify(definition.columnName)},`,
996
- ` type: ${JSON.stringify(definition.type)},`,
1208
+ `name: ${JSON.stringify(definition.columnName)}`,
1209
+ `type: ${JSON.stringify(definition.type)}`,
997
1210
  ];
998
1211
 
999
1212
  if (definition.length) {
1000
- properties.push(` length: ${JSON.stringify(definition.length)},`);
1213
+ properties.push(`length: ${JSON.stringify(definition.length)}`);
1001
1214
  }
1002
1215
  if (typeof definition.isNullable === "boolean") {
1003
- properties.push(` isNullable: ${definition.isNullable},`);
1216
+ properties.push(`isNullable: ${definition.isNullable}`);
1004
1217
  }
1005
1218
  if (definition.defaultExpression) {
1006
- properties.push(` default: ${JSON.stringify(definition.defaultExpression)},`);
1219
+ properties.push(`default: ${JSON.stringify(definition.defaultExpression)}`);
1007
1220
  }
1008
1221
  if (definition.isGenerated) {
1009
- properties.push(" isGenerated: true,");
1010
- properties.push(" generationStrategy: \"increment\",");
1222
+ properties.push("isGenerated: true");
1223
+ properties.push("generationStrategy: \"increment\"");
1011
1224
  }
1012
1225
  if (definition.isUnique) {
1013
- properties.push(" isUnique: true,");
1226
+ properties.push("isUnique: true");
1014
1227
  }
1015
1228
 
1016
- properties.push(" }");
1017
- return properties.join("\n");
1229
+ return `{ ${properties.join(", ")}, }`;
1230
+ }
1231
+
1232
+ private renderTableIndexLiteral(definition: { name: string; columnNames: string[]; isUnique?: boolean }) {
1233
+ const properties: string[] = [
1234
+ `name: ${JSON.stringify(definition.name)}`,
1235
+ `columnNames: ${JSON.stringify(definition.columnNames)}`,
1236
+ ];
1237
+
1238
+ if (definition.isUnique) {
1239
+ properties.push("isUnique: true");
1240
+ }
1241
+
1242
+ return `{ ${properties.join(", ")}, }`;
1018
1243
  }
1019
1244
 
1020
1245
  private buildMigrationObjectName(prefix: string, tableName: string, columnName: string) {
@@ -1050,13 +1275,15 @@ export class DatasourceIntrospectionService {
1050
1275
  private async buildTableInventory(datasource: DatasourceRecord) {
1051
1276
  const provider = this.getProvider(datasource.type);
1052
1277
  const runtimeDataSource = this.resolveDataSourceByName(datasource.name);
1053
- const [tables, columns, models] = await Promise.all([
1278
+ const [rawTables, rawColumns, models] = await Promise.all([
1054
1279
  provider.listTables(runtimeDataSource),
1055
1280
  provider.listColumns(runtimeDataSource),
1056
1281
  this.modelMetadataRepository.find({ relations: { module: true } as any }),
1057
1282
  ]);
1283
+ const tables = rawTables.filter((table) => !this.isSolidxInternalTableName(table.tableName));
1284
+ const columns = rawColumns.filter((column) => !this.isSolidxInternalTableName(column.tableName));
1058
1285
 
1059
- const modelMap = this.buildModelMap(models);
1286
+ const modelLookup = this.buildModelLookup(models);
1060
1287
  const columnsByKey = this.groupColumnsByTable(columns);
1061
1288
  const tableKeys = new Set<string>();
1062
1289
 
@@ -1080,7 +1307,7 @@ export class DatasourceIntrospectionService {
1080
1307
 
1081
1308
  const suggestedLegacyTableType = this.detectLegacyTableType(datasource.type, tableColumns);
1082
1309
  const suggestedIdColumn = this.getSuggestedIdColumn(datasource.type, tableColumns, suggestedLegacyTableType);
1083
- const mappedModel = modelMap.get(this.toModelMapKey(datasource.name, tableName)) ?? null;
1310
+ const mappedModel = this.resolveMappedModel(modelLookup, datasource.name, tableName);
1084
1311
  const physicalColumns = tableColumns.filter((column) => !this.isHandledBySuperclass(column.columnName, suggestedLegacyTableType));
1085
1312
 
1086
1313
  return {
@@ -1119,19 +1346,52 @@ export class DatasourceIntrospectionService {
1119
1346
  };
1120
1347
  }
1121
1348
 
1122
- private buildModelMap(models: ModelMetadata[]) {
1123
- const map = new Map<string, ModelMetadata>();
1349
+ private buildModelLookup(models: ModelMetadata[]) {
1350
+ const byDatasourceAndTable = new Map<string, ModelMetadata>();
1351
+ const byTableName = new Map<string, ModelMetadata[]>();
1124
1352
 
1125
1353
  for (const model of models) {
1126
1354
  if (!model.tableName) {
1127
1355
  continue;
1128
1356
  }
1129
1357
 
1130
- map.set(this.toModelMapKey(model.dataSource, model.tableName), model);
1131
- map.set(this.toModelMapKey("*", model.tableName), model);
1358
+ byDatasourceAndTable.set(this.toModelMapKey(model.dataSource, model.tableName), model);
1359
+
1360
+ const normalizedTableName = model.tableName.trim().toLowerCase();
1361
+ const bucket = byTableName.get(normalizedTableName);
1362
+ if (bucket) {
1363
+ bucket.push(model);
1364
+ continue;
1365
+ }
1366
+
1367
+ byTableName.set(normalizedTableName, [model]);
1368
+ }
1369
+
1370
+ return {
1371
+ byDatasourceAndTable,
1372
+ byTableName,
1373
+ };
1374
+ }
1375
+
1376
+ private resolveMappedModel(
1377
+ lookup: {
1378
+ byDatasourceAndTable: Map<string, ModelMetadata>;
1379
+ byTableName: Map<string, ModelMetadata[]>;
1380
+ },
1381
+ datasourceName: string,
1382
+ tableName: string,
1383
+ ) {
1384
+ const exactMatch = lookup.byDatasourceAndTable.get(this.toModelMapKey(datasourceName, tableName));
1385
+ if (exactMatch) {
1386
+ return exactMatch;
1387
+ }
1388
+
1389
+ const fallbackMatches = lookup.byTableName.get(tableName.trim().toLowerCase()) ?? [];
1390
+ if (fallbackMatches.length === 1) {
1391
+ return fallbackMatches[0];
1132
1392
  }
1133
1393
 
1134
- return map;
1394
+ return fallbackMatches[0] ?? null;
1135
1395
  }
1136
1396
 
1137
1397
  private groupColumnsByTable(columns: DatasourceIntrospectionColumn[]) {
@@ -1151,6 +1411,10 @@ export class DatasourceIntrospectionService {
1151
1411
  return grouped;
1152
1412
  }
1153
1413
 
1414
+ private isSolidxInternalTableName(tableName: string | null | undefined) {
1415
+ return typeof tableName === "string" && tableName.toLowerCase().startsWith("ss_");
1416
+ }
1417
+
1154
1418
  private detectLegacyTableType(datasourceType: string, columns: DatasourceIntrospectionColumn[]): LegacyTableType {
1155
1419
  const normalizedDatasourceType = datasourceType.toLowerCase();
1156
1420
  const hasExistingGeneratedId = columns.some((column) => column.isAutoGenerated);
@@ -1239,13 +1503,17 @@ export class DatasourceIntrospectionService {
1239
1503
  datasourceType: string,
1240
1504
  ): TableDetailColumnRecord {
1241
1505
  const superclassFieldName = this.getSuperclassFieldName(column.columnName, legacyTableType);
1506
+ const normalizedColumnName = column.columnName.toLowerCase();
1242
1507
  const fieldName = legacyTableType === LegacyTableType.EXISTING_ID && column.isAutoGenerated
1243
1508
  ? "id"
1244
- : this.toFieldName(column.columnName);
1509
+ : legacyTableType === LegacyTableType.GENERATED_ID && normalizedColumnName === "id"
1510
+ ? "legacyId"
1511
+ : this.toFieldName(column.columnName);
1245
1512
 
1246
1513
  return {
1247
1514
  columnName: column.columnName,
1248
1515
  fieldName,
1516
+ displayName: startCase(fieldName),
1249
1517
  dataType: column.dataType,
1250
1518
  solidFieldType: this.toSolidFieldType(column),
1251
1519
  ormType: this.toOrmFieldType(column, datasourceType),
@@ -1256,6 +1524,7 @@ export class DatasourceIntrospectionService {
1256
1524
  numericScale: column.numericScale,
1257
1525
  ordinalPosition: column.ordinalPosition,
1258
1526
  isAutoGenerated: column.isAutoGenerated,
1527
+ isPrimaryKey: column.isPrimaryKey,
1259
1528
  handledBySuperclass: Boolean(superclassFieldName),
1260
1529
  superclassFieldName,
1261
1530
  };
@@ -1298,109 +1567,80 @@ export class DatasourceIntrospectionService {
1298
1567
  return "LegacyCommonEntityWithExistingId";
1299
1568
  }
1300
1569
 
1301
- private toSolidFieldType(column: DatasourceIntrospectionColumn) {
1302
- const normalized = column.dataType.toLowerCase();
1303
- const charLength = column.characterMaximumLength ?? null;
1570
+ private getDatasourceTypeFamily(dataType: string): DatasourceTypeFamily | null {
1571
+ const normalized = dataType.toLowerCase();
1304
1572
 
1305
- if (["bit", "bool", "boolean"].includes(normalized)) {
1306
- return "boolean";
1307
- }
1308
-
1309
- if (["bigint"].includes(normalized)) {
1310
- return "bigint";
1311
- }
1312
-
1313
- if (["int", "integer", "smallint", "tinyint"].includes(normalized)) {
1314
- return "int";
1315
- }
1316
-
1317
- if (["numeric", "decimal", "float", "double", "real", "money", "smallmoney"].includes(normalized)) {
1318
- return "decimal";
1319
- }
1320
-
1321
- if (["date"].includes(normalized)) {
1322
- return "date";
1573
+ for (const [family, aliases] of Object.entries(DATASOURCE_TYPE_FAMILY_ALIASES) as Array<[DatasourceTypeFamily, string[]]>) {
1574
+ if (aliases.includes(normalized)) {
1575
+ return family;
1576
+ }
1323
1577
  }
1324
1578
 
1325
- if (["time"].includes(normalized)) {
1326
- return "time";
1579
+ if (normalized.includes("json")) {
1580
+ return "json";
1327
1581
  }
1328
1582
 
1329
- if (["datetime", "datetime2", "timestamp", "timestamptz", "smalldatetime"].includes(normalized) || normalized.includes("date")) {
1583
+ if (normalized.includes("date")) {
1330
1584
  return "datetime";
1331
1585
  }
1332
1586
 
1333
- if (["json", "jsonb", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json")) {
1334
- return "json";
1335
- }
1336
-
1337
- if (["uniqueidentifier", "uuid"].includes(normalized)) {
1338
- return "uuid";
1339
- }
1587
+ return null;
1588
+ }
1340
1589
 
1341
- if (["text", "ntext", "mediumtext", "longtext", "clob"].includes(normalized)) {
1342
- return "longText";
1343
- }
1590
+ private toSolidFieldType(column: DatasourceIntrospectionColumn) {
1591
+ const family = this.getDatasourceTypeFamily(column.dataType);
1592
+ const charLength = column.characterMaximumLength ?? null;
1344
1593
 
1345
- if (["varchar", "nvarchar", "char", "nchar", "citext"].includes(normalized)) {
1346
- if (charLength !== null && charLength > 255) {
1594
+ switch (family) {
1595
+ case "boolean":
1596
+ return "boolean";
1597
+ case "bigint":
1598
+ return "bigint";
1599
+ case "int":
1600
+ return "int";
1601
+ case "numeric":
1602
+ case "decimal":
1603
+ case "float":
1604
+ return "decimal";
1605
+ case "date":
1606
+ return "date";
1607
+ case "time":
1608
+ return "time";
1609
+ case "datetime":
1610
+ case "datetimeWithTimezone":
1611
+ return "datetime";
1612
+ case "json":
1613
+ case "jsonb":
1614
+ return "json";
1615
+ case "uuid":
1616
+ return "uuid";
1617
+ case "mediumText":
1618
+ case "longText":
1347
1619
  return "longText";
1348
- }
1620
+ case "shortText":
1621
+ case "unicodeShortText":
1622
+ if (charLength !== null && charLength > 255) {
1623
+ return "longText";
1624
+ }
1349
1625
 
1350
- return "shortText";
1626
+ return "shortText";
1627
+ default:
1628
+ return "shortText";
1351
1629
  }
1352
-
1353
- return "shortText";
1354
1630
  }
1355
1631
 
1356
1632
  private toOrmFieldType(column: DatasourceIntrospectionColumn, datasourceType: string) {
1357
- const normalized = column.dataType.toLowerCase();
1633
+ const family = this.getDatasourceTypeFamily(column.dataType);
1358
1634
  const normalizedDatasourceType = datasourceType.toLowerCase();
1635
+ const ormType = family
1636
+ ? ORM_TYPE_BY_DATASOURCE_FAMILY[normalizedDatasourceType]?.[family]
1637
+ : undefined;
1638
+
1639
+ if (ormType) {
1640
+ return ormType;
1641
+ }
1359
1642
 
1360
- if (normalizedDatasourceType === "mssql") {
1361
- if (["bit", "bool", "boolean"].includes(normalized)) return "bit";
1362
- if (["bigint"].includes(normalized)) return "bigint";
1363
- if (["int", "integer", "smallint", "tinyint"].includes(normalized)) return "int";
1364
- if (["numeric"].includes(normalized)) return "numeric";
1365
- if (["decimal", "float", "double", "real", "money", "smallmoney"].includes(normalized)) return "decimal";
1366
- if (["date"].includes(normalized)) return "date";
1367
- if (["time"].includes(normalized)) return "time";
1368
- if (["datetime2"].includes(normalized)) return "datetime2";
1369
- if (["datetime", "smalldatetime", "timestamp"].includes(normalized) || normalized.includes("date")) return "datetime";
1370
- if (["json", "jsonb", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json")) return "simple-json";
1371
- if (["uniqueidentifier", "uuid"].includes(normalized)) return "uniqueidentifier";
1372
- if (["text", "ntext", "mediumtext", "longtext", "clob"].includes(normalized)) return "text";
1373
- if (["nvarchar", "nchar"].includes(normalized)) return "nvarchar";
1374
- return "varchar";
1375
- }
1376
-
1377
- if (normalizedDatasourceType === "mysql") {
1378
- if (["bigint"].includes(normalized)) return "int";
1379
- if (["int", "integer", "smallint", "tinyint"].includes(normalized)) return "int";
1380
- if (["decimal", "numeric"].includes(normalized)) return "decimal";
1381
- if (["float", "double", "real", "money", "smallmoney"].includes(normalized)) return "double";
1382
- if (["bit", "bool", "boolean"].includes(normalized)) return "boolean";
1383
- if (["date"].includes(normalized)) return "date";
1384
- if (["time"].includes(normalized)) return "time";
1385
- if (["datetime", "timestamp", "smalldatetime"].includes(normalized) || normalized.includes("date")) return "datetime";
1386
- if (["json", "jsonb", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json")) return "json";
1387
- if (["mediumtext"].includes(normalized)) return "mediumtext";
1388
- if (["longtext", "text", "ntext", "clob"].includes(normalized)) return "longtext";
1389
- return "varchar";
1390
- }
1391
-
1392
- if (["bigint"].includes(normalized)) return "bigint";
1393
- if (["int", "integer", "smallint", "tinyint"].includes(normalized)) return "integer";
1394
- if (["decimal", "numeric", "float", "double", "real", "money", "smallmoney"].includes(normalized)) return "decimal";
1395
- if (["bit", "bool", "boolean"].includes(normalized)) return "boolean";
1396
- if (["date"].includes(normalized)) return "date";
1397
- if (["time"].includes(normalized)) return "time";
1398
- if (["timestamptz"].includes(normalized)) return "timestamptz";
1399
- if (["datetime", "datetime2", "timestamp", "smalldatetime"].includes(normalized) || normalized.includes("date")) return "timestamp";
1400
- if (["jsonb"].includes(normalized)) return "jsonb";
1401
- if (["json", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json")) return "json";
1402
- if (["text", "ntext", "mediumtext", "longtext", "clob"].includes(normalized)) return "text";
1403
- return "varchar";
1643
+ return normalizedDatasourceType === "postgres" ? "varchar" : "varchar";
1404
1644
  }
1405
1645
 
1406
1646
  private toFieldName(columnName: string) {
@@ -1449,6 +1689,269 @@ export class DatasourceIntrospectionService {
1449
1689
  return `${datasourceName.toLowerCase()}::${tableName.toLowerCase()}`;
1450
1690
  }
1451
1691
 
1692
+ private async syncTypeormDatasourceFile(moduleName: string, datasource: DatasourceRecord, singularName: string) {
1693
+ const datasourceFilePath = path.resolve(process.cwd(), "src", `typeorm-${datasource.name}-datasource.ts`);
1694
+ const entityClassName = classify(singularName);
1695
+ const entityImportPath = `./${moduleName}/entities/${kebabCase(singularName)}.entity`;
1696
+ const migrationGlob = `join(__dirname, './${moduleName}/migrations/${datasource.name}/*.{ts,js}')`;
1697
+
1698
+ const existingContent = await fs.readFile(datasourceFilePath, "utf8").catch(() => null);
1699
+ if (!existingContent) {
1700
+ const rendered = this.renderTypeormDatasourceTemplate({
1701
+ datasource,
1702
+ entityClassName,
1703
+ entityImportPath,
1704
+ migrationGlob,
1705
+ });
1706
+ await fs.writeFile(datasourceFilePath, rendered, "utf8");
1707
+ return {
1708
+ filePath: datasourceFilePath,
1709
+ created: true,
1710
+ updated: false,
1711
+ entityRegistered: true,
1712
+ migrationGlobRegistered: true,
1713
+ };
1714
+ }
1715
+
1716
+ const project = new Project({
1717
+ skipAddingFilesFromTsConfig: true,
1718
+ manipulationSettings: {
1719
+ quoteKind: QuoteKind.Single,
1720
+ indentationText: IndentationText.FourSpaces,
1721
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
1722
+ },
1723
+ });
1724
+ const sourceFile = project.createSourceFile(datasourceFilePath, existingContent, { overwrite: true });
1725
+
1726
+ const entityImportDeclaration = sourceFile.getImportDeclarations().find((declaration) =>
1727
+ declaration.getModuleSpecifierValue() === entityImportPath,
1728
+ );
1729
+ if (entityImportDeclaration) {
1730
+ const alreadyImported = entityImportDeclaration.getNamedImports().some((namedImport) => namedImport.getName() === entityClassName);
1731
+ if (!alreadyImported) {
1732
+ entityImportDeclaration.addNamedImport(entityClassName);
1733
+ }
1734
+ } else {
1735
+ sourceFile.addImportDeclaration({
1736
+ moduleSpecifier: entityImportPath,
1737
+ namedImports: [entityClassName],
1738
+ });
1739
+ }
1740
+
1741
+ const entitiesArray = sourceFile
1742
+ .getVariableDeclarationOrThrow("entities")
1743
+ .getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
1744
+ const entityAlreadyRegistered = this.arrayLiteralContainsValue(entitiesArray, entityClassName);
1745
+ if (!entityAlreadyRegistered) {
1746
+ entitiesArray.addElement(entityClassName);
1747
+ }
1748
+
1749
+ const dataSourceOptions = this.getTypeormDatasourceOptions(sourceFile);
1750
+ const migrationsArray = this.getObjectPropertyArrayLiteral(dataSourceOptions, "migrations");
1751
+ const migrationAlreadyRegistered = this.arrayLiteralContainsValue(migrationsArray, migrationGlob);
1752
+ if (!migrationAlreadyRegistered) {
1753
+ migrationsArray.addElement(migrationGlob);
1754
+ }
1755
+
1756
+ sourceFile.organizeImports();
1757
+ sourceFile.formatText();
1758
+
1759
+ const nextContent = sourceFile.getFullText();
1760
+ const updated = nextContent !== existingContent;
1761
+ if (updated) {
1762
+ await fs.writeFile(datasourceFilePath, nextContent, "utf8");
1763
+ }
1764
+
1765
+ return {
1766
+ filePath: datasourceFilePath,
1767
+ created: false,
1768
+ updated,
1769
+ entityRegistered: true,
1770
+ migrationGlobRegistered: true,
1771
+ };
1772
+ }
1773
+
1774
+ private renderTypeormDatasourceTemplate(input: {
1775
+ datasource: DatasourceRecord;
1776
+ entityClassName: string;
1777
+ entityImportPath: string;
1778
+ migrationGlob: string;
1779
+ }) {
1780
+ const { datasource, entityClassName, entityImportPath, migrationGlob } = input;
1781
+ const envPrefix = datasource.envPrefix?.toUpperCase?.() || "DEFAULT";
1782
+ const portFallback = datasource.type === "postgres" ? 5432 : datasource.type === "mysql" ? 3306 : 1433;
1783
+ const project = new Project({
1784
+ skipAddingFilesFromTsConfig: true,
1785
+ manipulationSettings: {
1786
+ quoteKind: QuoteKind.Single,
1787
+ indentationText: IndentationText.FourSpaces,
1788
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
1789
+ },
1790
+ });
1791
+ const sourceFile = project.createSourceFile("typeorm-template.ts", "", { overwrite: true });
1792
+
1793
+ sourceFile.addImportDeclaration({ moduleSpecifier: "reflect-metadata" });
1794
+ sourceFile.addImportDeclaration({ moduleSpecifier: "path", namedImports: ["join"] });
1795
+ sourceFile.addImportDeclaration({
1796
+ moduleSpecifier: "dotenv",
1797
+ namedImports: [{ name: "config", alias: "dotenvConfig" }],
1798
+ });
1799
+ sourceFile.addStatements("dotenvConfig({ path: join(__dirname, '../.env') });");
1800
+ sourceFile.addStatements("");
1801
+
1802
+ sourceFile.addImportDeclaration({ moduleSpecifier: "typeorm", namedImports: ["DataSource"] });
1803
+ sourceFile.addImportDeclaration({
1804
+ moduleSpecifier: "@solidxai/core",
1805
+ namespaceImport: "SolidCoreModuleExports",
1806
+ });
1807
+ sourceFile.addImportDeclaration({
1808
+ moduleSpecifier: "@solidxai/core",
1809
+ namedImports: ["getDynamicModuleNames", "parseBooleanEnv"],
1810
+ });
1811
+ sourceFile.addImportDeclaration({
1812
+ moduleSpecifier: "typeorm",
1813
+ namedImports: ["getMetadataArgsStorage"],
1814
+ });
1815
+ sourceFile.addImportDeclaration({
1816
+ moduleSpecifier: "typeorm-naming-strategies",
1817
+ namedImports: ["SnakeNamingStrategy"],
1818
+ });
1819
+ sourceFile.addImportDeclaration({
1820
+ moduleSpecifier: entityImportPath,
1821
+ namedImports: [entityClassName],
1822
+ });
1823
+
1824
+ sourceFile.addFunction({
1825
+ name: "getEntitiesFromExports",
1826
+ parameters: [{ name: "exports", type: "Record<string, any>" }],
1827
+ statements: (writer) => {
1828
+ writer.writeLine("const metadataStorage = getMetadataArgsStorage();");
1829
+ writer.writeLine("return Object.values(exports).filter((item) =>");
1830
+ writer.indent(() => {
1831
+ writer.writeLine("metadataStorage.tables.some((table) => table.target === item),");
1832
+ });
1833
+ writer.write(");");
1834
+ },
1835
+ });
1836
+
1837
+ sourceFile.addVariableStatement({
1838
+ declarationKind: VariableDeclarationKind.Const,
1839
+ declarations: [
1840
+ {
1841
+ name: "coreEntities",
1842
+ initializer: "getEntitiesFromExports(SolidCoreModuleExports)",
1843
+ },
1844
+ ],
1845
+ });
1846
+ sourceFile.addVariableStatement({
1847
+ declarationKind: VariableDeclarationKind.Const,
1848
+ declarations: [
1849
+ {
1850
+ name: "dynamicModules",
1851
+ initializer: "getDynamicModuleNames()",
1852
+ },
1853
+ ],
1854
+ });
1855
+ sourceFile.addVariableStatement({
1856
+ declarationKind: VariableDeclarationKind.Const,
1857
+ declarations: [
1858
+ {
1859
+ name: "entities",
1860
+ initializer: `[\n ...coreEntities,\n ${entityClassName},\n]`,
1861
+ },
1862
+ ],
1863
+ });
1864
+ sourceFile.addVariableStatement({
1865
+ declarationKind: VariableDeclarationKind.Const,
1866
+ declarations: [
1867
+ {
1868
+ name: "logging",
1869
+ initializer: `parseBooleanEnv('${envPrefix}_DATABASE_LOGGING')`,
1870
+ },
1871
+ ],
1872
+ });
1873
+
1874
+ sourceFile.addStatements("// IMPORTANT: synchronize must be false when using migrations");
1875
+ sourceFile.addVariableStatement({
1876
+ declarationKind: VariableDeclarationKind.Const,
1877
+ isExported: true,
1878
+ declarations: [
1879
+ {
1880
+ name: "DefaultDataSource",
1881
+ initializer: (writer) => {
1882
+ writer.write("new DataSource(");
1883
+ writer.inlineBlock(() => {
1884
+ writer.writeLine(`type: '${datasource.type}',`);
1885
+ writer.blankLine();
1886
+ writer.writeLine(`host: process.env.${envPrefix}_DATABASE_HOST,`);
1887
+ writer.writeLine(`port: +(process.env.${envPrefix}_DATABASE_PORT || ${portFallback}),`);
1888
+ writer.writeLine(`username: process.env.${envPrefix}_DATABASE_USER,`);
1889
+ writer.writeLine(`password: process.env.${envPrefix}_DATABASE_PASSWORD,`);
1890
+ writer.writeLine(`database: process.env.${envPrefix}_DATABASE_NAME,`);
1891
+ writer.blankLine();
1892
+ writer.writeLine("entities,");
1893
+ writer.writeLine("migrations: [");
1894
+ writer.indent(() => {
1895
+ writer.writeLine(`${migrationGlob},`);
1896
+ });
1897
+ writer.writeLine("],");
1898
+ writer.writeLine("synchronize: false,");
1899
+ writer.writeLine("logging,");
1900
+ writer.writeLine("namingStrategy: new SnakeNamingStrategy(),");
1901
+ if (datasource.type === "mssql") {
1902
+ writer.writeLine("options: {");
1903
+ writer.indent(() => {
1904
+ writer.writeLine("encrypt: false,");
1905
+ writer.writeLine("trustServerCertificate: true,");
1906
+ });
1907
+ writer.writeLine("},");
1908
+ }
1909
+ });
1910
+ writer.write(")");
1911
+ },
1912
+ },
1913
+ ],
1914
+ });
1915
+
1916
+ sourceFile.organizeImports();
1917
+ sourceFile.formatText();
1918
+ return sourceFile.getFullText();
1919
+ }
1920
+
1921
+ private arrayLiteralContainsValue(arrayLiteral: ArrayLiteralExpression, rawValue: string) {
1922
+ const normalizedTarget = rawValue.replace(/\s+/g, "");
1923
+ return arrayLiteral.getElements().some((element) => element.getText().replace(/\s+/g, "") === normalizedTarget);
1924
+ }
1925
+
1926
+ private getTypeormDatasourceOptions(sourceFile: import("ts-morph").SourceFile) {
1927
+ const defaultDataSourceDeclaration = sourceFile.getVariableDeclarationOrThrow("DefaultDataSource");
1928
+ const initializer = defaultDataSourceDeclaration.getInitializer();
1929
+ if (!initializer || !Node.isNewExpression(initializer)) {
1930
+ throw new BadRequestException("Unable to locate DefaultDataSource initializer in datasource file.");
1931
+ }
1932
+
1933
+ const options = initializer.getArguments()[0];
1934
+ if (!options || !Node.isObjectLiteralExpression(options)) {
1935
+ throw new BadRequestException("Unable to locate DefaultDataSource options object in datasource file.");
1936
+ }
1937
+
1938
+ return options;
1939
+ }
1940
+
1941
+ private getObjectPropertyArrayLiteral(objectLiteral: ObjectLiteralExpression, propertyName: string) {
1942
+ const property = objectLiteral.getProperty(propertyName);
1943
+ if (!property || !Node.isPropertyAssignment(property)) {
1944
+ throw new BadRequestException(`Unable to locate "${propertyName}" property in datasource file.`);
1945
+ }
1946
+
1947
+ const initializer = property.getInitializer();
1948
+ if (!initializer || !Node.isArrayLiteralExpression(initializer)) {
1949
+ throw new BadRequestException(`Unable to locate "${propertyName}" array in datasource file.`);
1950
+ }
1951
+
1952
+ return initializer;
1953
+ }
1954
+
1452
1955
  private buildSynchronizeBlockedMessage(datasource: DatasourceRecord) {
1453
1956
  return `Datasource "${datasource.displayName}" has synchronize enabled. Set ${datasource.envPrefix}_DATABASE_SYNCHRONIZE=false before starting legacy table introspection.`;
1454
1957
  }