@solidxai/core 0.1.10 → 0.1.11-beta.0

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 (113) hide show
  1. package/dist/controllers/datasource-introspection.controller.d.ts +234 -0
  2. package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -0
  3. package/dist/controllers/datasource-introspection.controller.js +98 -0
  4. package/dist/controllers/datasource-introspection.controller.js.map +1 -0
  5. package/dist/controllers/datasource-management.controller.d.ts +18 -0
  6. package/dist/controllers/datasource-management.controller.d.ts.map +1 -0
  7. package/dist/controllers/datasource-management.controller.js +55 -0
  8. package/dist/controllers/datasource-management.controller.js.map +1 -0
  9. package/dist/dtos/create-datasource-management.dto.d.ts +25 -0
  10. package/dist/dtos/create-datasource-management.dto.d.ts.map +1 -0
  11. package/dist/dtos/create-datasource-management.dto.js +169 -0
  12. package/dist/dtos/create-datasource-management.dto.js.map +1 -0
  13. package/dist/dtos/datasource-introspection-mapping.dto.d.ts +45 -0
  14. package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -0
  15. package/dist/dtos/datasource-introspection-mapping.dto.js +233 -0
  16. package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -0
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/interfaces/datasource-introspection-provider.interface.d.ts +26 -0
  22. package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -0
  23. package/dist/interfaces/datasource-introspection-provider.interface.js +3 -0
  24. package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -0
  25. package/dist/seeders/seed-data/solid-core-metadata.json +21 -0
  26. package/dist/services/authentication.service.js +1 -1
  27. package/dist/services/authentication.service.js.map +1 -1
  28. package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.d.ts.map +1 -1
  29. package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.js +2 -2
  30. package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.js.map +1 -1
  31. package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.d.ts.map +1 -1
  32. package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.js +2 -2
  33. package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.js.map +1 -1
  34. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.d.ts +3 -0
  35. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.d.ts.map +1 -1
  36. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.js +71 -0
  37. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.js.map +1 -1
  38. package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.d.ts.map +1 -1
  39. package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.js +3 -3
  40. package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.js.map +1 -1
  41. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts +11 -0
  42. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -0
  43. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +181 -0
  44. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -0
  45. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts +11 -0
  46. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -0
  47. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +90 -0
  48. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -0
  49. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts +11 -0
  50. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -0
  51. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +156 -0
  52. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -0
  53. package/dist/services/datasource-introspection.service.d.ts +288 -0
  54. package/dist/services/datasource-introspection.service.d.ts.map +1 -0
  55. package/dist/services/datasource-introspection.service.js +1278 -0
  56. package/dist/services/datasource-introspection.service.js.map +1 -0
  57. package/dist/services/datasource-management.service.d.ts +60 -0
  58. package/dist/services/datasource-management.service.d.ts.map +1 -0
  59. package/dist/services/datasource-management.service.js +461 -0
  60. package/dist/services/datasource-management.service.js.map +1 -0
  61. package/dist/services/field-metadata.service.d.ts.map +1 -1
  62. package/dist/services/field-metadata.service.js +19 -0
  63. package/dist/services/field-metadata.service.js.map +1 -1
  64. package/dist/services/model-metadata.service.d.ts +1 -3
  65. package/dist/services/model-metadata.service.d.ts.map +1 -1
  66. package/dist/services/model-metadata.service.js +47 -15
  67. package/dist/services/model-metadata.service.js.map +1 -1
  68. package/dist/services/role-metadata.service.d.ts +1 -0
  69. package/dist/services/role-metadata.service.d.ts.map +1 -1
  70. package/dist/services/role-metadata.service.js +13 -0
  71. package/dist/services/role-metadata.service.js.map +1 -1
  72. package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.d.ts.map +1 -1
  73. package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.js +2 -3
  74. package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.js.map +1 -1
  75. package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.d.ts.map +1 -1
  76. package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.js +2 -3
  77. package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.js.map +1 -1
  78. package/dist/services/setting.service.d.ts +1 -0
  79. package/dist/services/setting.service.d.ts.map +1 -1
  80. package/dist/services/setting.service.js +32 -8
  81. package/dist/services/setting.service.js.map +1 -1
  82. package/dist/services/solid-ts-morph.service.d.ts +1 -1
  83. package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
  84. package/dist/services/solid-ts-morph.service.js.map +1 -1
  85. package/dist/solid-core.module.d.ts.map +1 -1
  86. package/dist/solid-core.module.js +16 -0
  87. package/dist/solid-core.module.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/controllers/datasource-introspection.controller.ts +58 -0
  90. package/src/controllers/datasource-management.controller.ts +24 -0
  91. package/src/dtos/create-datasource-management.dto.ts +139 -0
  92. package/src/dtos/datasource-introspection-mapping.dto.ts +182 -0
  93. package/src/index.ts +5 -0
  94. package/src/interfaces/datasource-introspection-provider.interface.ts +28 -0
  95. package/src/seeders/seed-data/solid-core-metadata.json +22 -1
  96. package/src/services/authentication.service.ts +1 -1
  97. package/src/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.ts +9 -4
  98. package/src/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.ts +9 -4
  99. package/src/services/dashboard-providers/mq-dashboard-provider-utils.ts +82 -0
  100. package/src/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.ts +10 -4
  101. package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +185 -0
  102. package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +95 -0
  103. package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +160 -0
  104. package/src/services/datasource-introspection.service.ts +1536 -0
  105. package/src/services/datasource-management.service.ts +566 -0
  106. package/src/services/field-metadata.service.ts +21 -1
  107. package/src/services/model-metadata.service.ts +50 -13
  108. package/src/services/role-metadata.service.ts +14 -0
  109. package/src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts +2 -4
  110. package/src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts +2 -4
  111. package/src/services/setting.service.ts +43 -12
  112. package/src/services/solid-ts-morph.service.ts +2 -2
  113. package/src/solid-core.module.ts +16 -0
@@ -0,0 +1,1278 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.DatasourceIntrospectionService = void 0;
46
+ const common_1 = require("@nestjs/common");
47
+ const core_1 = require("@nestjs/core");
48
+ const typeorm_1 = require("@nestjs/typeorm");
49
+ const crypto_1 = require("crypto");
50
+ const fs = __importStar(require("fs/promises"));
51
+ const path = __importStar(require("path"));
52
+ const lodash_1 = require("lodash");
53
+ const legacy_table_type_enum_1 = require("../enums/legacy-table-type.enum");
54
+ const module_metadata_helper_service_1 = require("../helpers/module-metadata-helper.service");
55
+ const model_metadata_helper_service_1 = require("../helpers/model-metadata-helper.service");
56
+ const model_metadata_repository_1 = require("../repository/model-metadata.repository");
57
+ const module_metadata_repository_1 = require("../repository/module-metadata.repository");
58
+ const datasource_management_service_1 = require("./datasource-management.service");
59
+ const field_metadata_service_1 = require("./field-metadata.service");
60
+ const model_metadata_service_1 = require("./model-metadata.service");
61
+ const mssql_datasource_introspection_provider_service_1 = require("./datasource-introspection/mssql-datasource-introspection-provider.service");
62
+ const mysql_datasource_introspection_provider_service_1 = require("./datasource-introspection/mysql-datasource-introspection-provider.service");
63
+ const postgres_datasource_introspection_provider_service_1 = require("./datasource-introspection/postgres-datasource-introspection-provider.service");
64
+ let DatasourceIntrospectionService = class DatasourceIntrospectionService {
65
+ constructor(moduleRef, datasourceManagementService, moduleMetadataRepository, modelMetadataRepository, moduleMetadataHelperService, modelMetadataHelperService, fieldMetadataService, modelMetadataService, mssqlProvider, mysqlProvider, postgresProvider) {
66
+ this.moduleRef = moduleRef;
67
+ this.datasourceManagementService = datasourceManagementService;
68
+ this.moduleMetadataRepository = moduleMetadataRepository;
69
+ this.modelMetadataRepository = modelMetadataRepository;
70
+ this.moduleMetadataHelperService = moduleMetadataHelperService;
71
+ this.modelMetadataHelperService = modelMetadataHelperService;
72
+ this.fieldMetadataService = fieldMetadataService;
73
+ this.modelMetadataService = modelMetadataService;
74
+ this.mssqlProvider = mssqlProvider;
75
+ this.mysqlProvider = mysqlProvider;
76
+ this.postgresProvider = postgresProvider;
77
+ this.supportedDatasourceTypes = new Set(["mssql", "mysql", "postgres"]);
78
+ }
79
+ async getBootstrap(moduleId) {
80
+ const module = await this.loadModule(moduleId);
81
+ const datasources = this.datasourceManagementService
82
+ .listRecords()
83
+ .filter((record) => this.supportedDatasourceTypes.has(record.type?.toLowerCase?.() ?? ""));
84
+ return {
85
+ data: {
86
+ module: this.toModuleSummary(module),
87
+ datasources: datasources.map((record) => this.toDatasourceSummary(record)),
88
+ recommendedDatasourceName: datasources.find((record) => record.name === module.defaultDataSource)?.name
89
+ ?? datasources[0]?.name
90
+ ?? null,
91
+ },
92
+ };
93
+ }
94
+ async getTables(moduleId, datasourceName) {
95
+ const module = await this.loadModule(moduleId);
96
+ const datasource = this.requireDatasource(datasourceName);
97
+ if (datasource.synchronize === true) {
98
+ return {
99
+ data: {
100
+ module: this.toModuleSummary(module),
101
+ datasource: this.toDatasourceSummary(datasource),
102
+ synchronizeBlocked: true,
103
+ synchronizeMessage: this.buildSynchronizeBlockedMessage(datasource),
104
+ records: [],
105
+ meta: {
106
+ totalRecords: 0,
107
+ },
108
+ },
109
+ };
110
+ }
111
+ const inventory = await this.buildTableInventory(datasource);
112
+ return {
113
+ data: {
114
+ module: this.toModuleSummary(module),
115
+ datasource: this.toDatasourceSummary(datasource),
116
+ synchronizeBlocked: false,
117
+ synchronizeMessage: null,
118
+ records: inventory.records,
119
+ meta: {
120
+ totalRecords: inventory.records.length,
121
+ },
122
+ },
123
+ };
124
+ }
125
+ async getTableDetail(moduleId, datasourceName, tableName, schema) {
126
+ const module = await this.loadModule(moduleId);
127
+ const datasource = this.requireDatasource(datasourceName);
128
+ if (datasource.synchronize === true) {
129
+ throw new common_1.BadRequestException(this.buildSynchronizeBlockedMessage(datasource));
130
+ }
131
+ const inventory = await this.buildTableInventory(datasource);
132
+ const tableKey = this.toTableKey(schema ?? null, tableName);
133
+ const selectedTable = inventory.recordsByKey.get(tableKey);
134
+ if (!selectedTable) {
135
+ throw new common_1.NotFoundException(`Table "${tableName}" was not found on datasource "${datasource.name}".`);
136
+ }
137
+ const provider = this.getProvider(datasource.type);
138
+ const runtimeDataSource = this.resolveDataSourceByName(datasource.name);
139
+ const rawColumns = inventory.columnsByKey.get(tableKey) ?? [];
140
+ const detailColumns = rawColumns.map((column) => this.toDetailColumnRecord(column, selectedTable.suggestedLegacyTableType, datasource.type));
141
+ const userKeyCandidates = this.buildUserKeyCandidates(rawColumns);
142
+ const suggestedSingularName = this.toSingularNameCandidate(tableName);
143
+ const suggestedPluralName = this.toPluralNameCandidate(tableName, suggestedSingularName);
144
+ const rawDdl = await this.buildRawDdl(provider, runtimeDataSource, {
145
+ tableName: selectedTable.tableName,
146
+ tableSchema: selectedTable.schema,
147
+ });
148
+ return {
149
+ data: {
150
+ module: this.toModuleSummary(module),
151
+ datasource: this.toDatasourceSummary(datasource),
152
+ table: selectedTable,
153
+ columns: detailColumns,
154
+ rawDdl,
155
+ userKeyCandidates,
156
+ suggestedMetadata: {
157
+ singularName: suggestedSingularName,
158
+ pluralName: suggestedPluralName,
159
+ displayName: (0, lodash_1.startCase)(suggestedSingularName),
160
+ tableName: selectedTable.tableName,
161
+ schema: selectedTable.schema,
162
+ dataSource: datasource.name,
163
+ dataSourceType: datasource.type,
164
+ legacyTableType: selectedTable.suggestedLegacyTableType,
165
+ baseClassName: selectedTable.suggestedBaseClassName,
166
+ userKeyField: selectedTable.suggestedUserKeyField,
167
+ synchronize: false,
168
+ },
169
+ },
170
+ };
171
+ }
172
+ async previewMapping(moduleId, mappingDto) {
173
+ const plan = await this.buildMappingPreview(moduleId, mappingDto);
174
+ return {
175
+ data: {
176
+ operation: plan.operation,
177
+ mappedModelId: plan.mappedModelId,
178
+ metadataJson: plan.metadataJson,
179
+ migration: plan.migration,
180
+ summary: plan.summary,
181
+ },
182
+ };
183
+ }
184
+ async applyMapping(moduleId, mappingDto) {
185
+ const plan = await this.buildMappingPreview(moduleId, mappingDto);
186
+ if (plan.operation === "update" && !mappingDto.modelId) {
187
+ throw new common_1.BadRequestException("Mapped tables require a model id before changes can be applied.");
188
+ }
189
+ const persistedModel = plan.operation === "update" && mappingDto.modelId
190
+ ? await this.modelMetadataService.update(mappingDto.modelId, plan.modelPayload)
191
+ : await this.modelMetadataService.create(plan.modelPayload);
192
+ if (plan.migration.willGenerate && plan.migration.content && plan.migration.filePath) {
193
+ await fs.mkdir(path.dirname(plan.migration.filePath), { recursive: true });
194
+ await fs.writeFile(plan.migration.filePath, plan.migration.content, "utf8");
195
+ }
196
+ const persistedModelId = mappingDto.modelId ?? persistedModel?.id ?? null;
197
+ return {
198
+ data: {
199
+ modelId: persistedModelId,
200
+ operation: plan.operation,
201
+ metadataJson: plan.metadataJson,
202
+ migration: {
203
+ ...plan.migration,
204
+ written: Boolean(plan.migration.willGenerate && plan.migration.filePath),
205
+ },
206
+ summary: plan.summary,
207
+ },
208
+ };
209
+ }
210
+ async buildMappingPreview(moduleId, mappingDto) {
211
+ const module = await this.loadModule(moduleId);
212
+ const datasource = this.requireDatasource(mappingDto.dataSource);
213
+ if (datasource.synchronize === true) {
214
+ throw new common_1.BadRequestException(this.buildSynchronizeBlockedMessage(datasource));
215
+ }
216
+ const inventory = await this.buildTableInventory(datasource);
217
+ const tableKey = this.toTableKey(mappingDto.schema ?? null, mappingDto.tableName);
218
+ const tableRecord = inventory.recordsByKey.get(tableKey);
219
+ if (!tableRecord) {
220
+ throw new common_1.NotFoundException(`Table "${mappingDto.tableName}" was not found on datasource "${datasource.name}".`);
221
+ }
222
+ const existingModel = mappingDto.mapped && mappingDto.modelId
223
+ ? await this.modelMetadataRepository.findOne({
224
+ where: { id: mappingDto.modelId },
225
+ relations: ["fields", "module", "parentModel"],
226
+ order: {
227
+ fields: {
228
+ id: "ASC",
229
+ },
230
+ },
231
+ })
232
+ : null;
233
+ if (mappingDto.mapped && !existingModel) {
234
+ throw new common_1.NotFoundException(`Mapped model "${mappingDto.modelId}" was not found.`);
235
+ }
236
+ const reviewedModel = this.normalizeReviewedMetadataModel(mappingDto.reviewedModel, mappingDto);
237
+ const modelPayload = reviewedModel
238
+ ? this.buildModelPayloadFromReviewedModel(moduleId, mappingDto, existingModel, reviewedModel)
239
+ : this.buildModelPayload(moduleId, mappingDto, existingModel);
240
+ const metadataJson = reviewedModel
241
+ ? await this.buildMetadataJsonPreviewFromReviewedModel(module.name, reviewedModel)
242
+ : await this.buildMetadataJsonPreview(module.name, mappingDto, existingModel);
243
+ const migration = await this.buildMigrationPreview(module.name, datasource, mappingDto, inventory.columnsByKey.get(tableKey) ?? []);
244
+ const includedColumnNames = mappingDto.columns
245
+ .filter((column) => !column.handledBySuperclass && column.include)
246
+ .map((column) => column.columnName);
247
+ const removedColumnNames = mappingDto.columns
248
+ .filter((column) => column.matched && !column.handledBySuperclass && !column.include)
249
+ .map((column) => column.columnName);
250
+ return {
251
+ operation: mappingDto.mapped ? "update" : "create",
252
+ mappedModelId: mappingDto.modelId ?? null,
253
+ metadataJson,
254
+ migration,
255
+ summary: {
256
+ includedColumnNames,
257
+ removedColumnNames,
258
+ missingSystemColumns: migration.missingSystemColumns,
259
+ generatedSystemColumnCount: migration.missingSystemColumns.length,
260
+ },
261
+ modelPayload,
262
+ };
263
+ }
264
+ buildModelPayload(moduleId, mappingDto, existingModel) {
265
+ const includedColumns = mappingDto.columns.filter((column) => !column.handledBySuperclass && column.include);
266
+ const matchedFieldIds = new Set(mappingDto.columns
267
+ .filter((column) => typeof column.fieldId === "number")
268
+ .map((column) => column.fieldId));
269
+ const preservedFieldPayloads = existingModel
270
+ ? existingModel.fields
271
+ .filter((field) => field.isSystem || !matchedFieldIds.has(field.id))
272
+ .map((field) => this.sanitizeExistingFieldPayload(field))
273
+ : [];
274
+ const updatedFieldPayloads = includedColumns.map((column) => {
275
+ const existingField = existingModel?.fields?.find((field) => field.id === column.fieldId) ?? null;
276
+ return this.buildFieldPayload(column, mappingDto, existingField);
277
+ });
278
+ return {
279
+ singularName: mappingDto.singularName.trim(),
280
+ pluralName: mappingDto.pluralName.trim(),
281
+ displayName: mappingDto.displayName.trim(),
282
+ description: mappingDto.description.trim() || mappingDto.displayName.trim(),
283
+ dataSource: mappingDto.dataSource,
284
+ dataSourceType: mappingDto.dataSourceType,
285
+ tableName: mappingDto.tableName,
286
+ enableSoftDelete: mappingDto.enableSoftDelete,
287
+ enableAuditTracking: mappingDto.enableAuditTracking,
288
+ internationalisation: mappingDto.internationalisation,
289
+ draftPublishWorkflow: mappingDto.draftPublishWorkflow,
290
+ moduleId,
291
+ fields: [...preservedFieldPayloads, ...updatedFieldPayloads],
292
+ isSystem: false,
293
+ isChild: false,
294
+ parentModelId: null,
295
+ parentModelUserKey: "",
296
+ legacyTableType: mappingDto.legacyTableType,
297
+ };
298
+ }
299
+ buildModelPayloadFromReviewedModel(moduleId, mappingDto, existingModel, reviewedModel) {
300
+ const systemFieldPayloads = existingModel
301
+ ? existingModel.fields
302
+ .filter((field) => field.isSystem)
303
+ .map((field) => this.sanitizeExistingFieldPayload(field))
304
+ : [];
305
+ const reviewedFieldPayloads = reviewedModel.fields.map((field) => {
306
+ const matchedExistingField = this.matchExistingFieldForReviewedModel(field, mappingDto, existingModel);
307
+ return {
308
+ ...field,
309
+ ...(matchedExistingField?.id ? { id: matchedExistingField.id } : {}),
310
+ isUserKey: field.name === reviewedModel.userKeyFieldUserKey,
311
+ isSystem: field.isSystem ?? false,
312
+ };
313
+ });
314
+ return {
315
+ singularName: reviewedModel.singularName.trim(),
316
+ pluralName: reviewedModel.pluralName.trim(),
317
+ displayName: reviewedModel.displayName.trim(),
318
+ description: reviewedModel.description.trim() || reviewedModel.displayName.trim(),
319
+ dataSource: reviewedModel.dataSource,
320
+ dataSourceType: reviewedModel.dataSourceType,
321
+ tableName: reviewedModel.tableName,
322
+ enableSoftDelete: reviewedModel.enableSoftDelete,
323
+ enableAuditTracking: reviewedModel.enableAuditTracking,
324
+ internationalisation: reviewedModel.internationalisation,
325
+ draftPublishWorkflow: reviewedModel.draftPublishWorkflow,
326
+ moduleId,
327
+ fields: [...systemFieldPayloads, ...reviewedFieldPayloads],
328
+ isSystem: false,
329
+ isChild: false,
330
+ parentModelId: null,
331
+ parentModelUserKey: reviewedModel.parentModelUserKey ?? "",
332
+ legacyTableType: reviewedModel.legacyTableType,
333
+ };
334
+ }
335
+ async buildMetadataJsonPreview(moduleName, mappingDto, existingModel) {
336
+ const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
337
+ const previewFields = await this.buildMetadataPreviewFields(mappingDto, existingModel);
338
+ return {
339
+ filePath,
340
+ model: {
341
+ singularName: mappingDto.singularName.trim(),
342
+ pluralName: mappingDto.pluralName.trim(),
343
+ displayName: mappingDto.displayName.trim(),
344
+ description: mappingDto.description.trim() || mappingDto.displayName.trim(),
345
+ dataSource: mappingDto.dataSource,
346
+ dataSourceType: mappingDto.dataSourceType,
347
+ tableName: mappingDto.tableName,
348
+ userKeyFieldUserKey: mappingDto.userKeyField,
349
+ isChild: false,
350
+ legacyTableType: mappingDto.legacyTableType,
351
+ parentModelUserKey: "",
352
+ enableAuditTracking: mappingDto.enableAuditTracking,
353
+ enableSoftDelete: mappingDto.enableSoftDelete,
354
+ draftPublishWorkflow: mappingDto.draftPublishWorkflow,
355
+ internationalisation: mappingDto.internationalisation,
356
+ fields: previewFields,
357
+ },
358
+ };
359
+ }
360
+ async buildMetadataJsonPreviewFromReviewedModel(moduleName, reviewedModel) {
361
+ const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
362
+ return {
363
+ filePath,
364
+ model: reviewedModel,
365
+ };
366
+ }
367
+ async buildMetadataPreviewFields(mappingDto, existingModel) {
368
+ const finalFields = new Map();
369
+ const existingFields = existingModel?.fields ?? [];
370
+ const matchedFieldIds = new Set(mappingDto.columns
371
+ .filter((column) => typeof column.fieldId === "number")
372
+ .map((column) => column.fieldId));
373
+ const includedColumns = mappingDto.columns.filter((column) => !column.handledBySuperclass && column.include);
374
+ for (const field of existingFields) {
375
+ if (field.isSystem || !matchedFieldIds.has(field.id)) {
376
+ finalFields.set(this.toFieldPreviewKey(field.id, field.name), this.sanitizeExistingFieldPayload(field));
377
+ }
378
+ }
379
+ for (const column of includedColumns) {
380
+ const existingField = existingFields.find((field) => field.id === column.fieldId) ?? null;
381
+ const fieldPayload = this.buildFieldPayload(column, mappingDto, existingField);
382
+ finalFields.set(this.toFieldPreviewKey(fieldPayload.id ?? null, fieldPayload.name), fieldPayload);
383
+ }
384
+ for (const column of mappingDto.columns.filter((entry) => entry.matched && !entry.include && entry.fieldId)) {
385
+ const existingField = existingFields.find((field) => field.id === column.fieldId);
386
+ if (!existingField || existingField.isSystem) {
387
+ continue;
388
+ }
389
+ finalFields.set(this.toFieldPreviewKey(existingField.id, existingField.name), {
390
+ ...this.sanitizeExistingFieldPayload(existingField),
391
+ isMarkedForRemoval: true,
392
+ isUserKey: false,
393
+ });
394
+ }
395
+ const serialized = Array.from(finalFields.values())
396
+ .filter((field) => !field.isSystem)
397
+ .map((field) => ({
398
+ ...field,
399
+ isUserKey: field.name === mappingDto.userKeyField,
400
+ }))
401
+ .sort((left, right) => {
402
+ const leftOrder = typeof left.id === "number" ? left.id : Number.MAX_SAFE_INTEGER;
403
+ const rightOrder = typeof right.id === "number" ? right.id : Number.MAX_SAFE_INTEGER;
404
+ if (leftOrder !== rightOrder) {
405
+ return leftOrder - rightOrder;
406
+ }
407
+ return `${left.name}`.localeCompare(`${right.name}`);
408
+ });
409
+ const previewFields = [];
410
+ for (const field of serialized) {
411
+ previewFields.push(await this.fieldMetadataService.createFieldConfig(field));
412
+ }
413
+ return previewFields;
414
+ }
415
+ async buildMigrationPreview(moduleName, datasource, mappingDto, tableColumns) {
416
+ const systemColumnDefinitions = this.getLegacySystemColumnDefinitions(mappingDto.legacyTableType, datasource.type);
417
+ const existingColumns = new Set(tableColumns.map((column) => column.columnName.toLowerCase()));
418
+ const missingSystemColumns = systemColumnDefinitions.filter((definition) => !existingColumns.has(definition.columnName.toLowerCase()));
419
+ const removedLegacyMappings = mappingDto.columns.filter((column) => column.matched && !column.include && !column.handledBySuperclass);
420
+ if (!missingSystemColumns.length) {
421
+ return {
422
+ willGenerate: false,
423
+ filePath: null,
424
+ fileName: null,
425
+ className: null,
426
+ timestamp: mappingDto.plannedMigrationTimestamp ?? null,
427
+ content: null,
428
+ note: removedLegacyMappings.length
429
+ ? "No migration will be generated for this change because it only updates metadata mapping for existing legacy columns."
430
+ : "All required SolidX system columns already exist on this table, so no migration will be generated.",
431
+ missingSystemColumns: [],
432
+ };
433
+ }
434
+ const modulePath = await this.moduleMetadataHelperService.getModulePath(moduleName);
435
+ const migrationTimestamp = mappingDto.plannedMigrationTimestamp ?? Date.now();
436
+ const migrationBaseName = `Sync${(0, lodash_1.startCase)(this.toSingularNameCandidate(mappingDto.tableName)).replace(/\s+/g, "")}SolidxSystemFields`;
437
+ const className = `${migrationBaseName}${migrationTimestamp}`;
438
+ const fileName = `${migrationTimestamp}-${migrationBaseName}.ts`;
439
+ const filePath = path.join(modulePath, "migrations", datasource.name, fileName);
440
+ return {
441
+ willGenerate: true,
442
+ filePath,
443
+ fileName,
444
+ className,
445
+ timestamp: migrationTimestamp,
446
+ content: this.renderMigrationTemplate(className, mappingDto, missingSystemColumns),
447
+ note: `Saving this mapping will also write a template migration for the missing SolidX system columns: ${missingSystemColumns.map((column) => column.columnName).join(", ")}.`,
448
+ missingSystemColumns: missingSystemColumns.map((column) => column.columnName),
449
+ };
450
+ }
451
+ sanitizeExistingFieldPayload(field) {
452
+ const { createdAt, updatedAt, deletedAt, model, mediaStorageProvider, identifier, ...rest } = field ?? {};
453
+ if (mediaStorageProvider?.id && !rest.mediaStorageProviderId) {
454
+ rest.mediaStorageProviderId = mediaStorageProvider.id;
455
+ }
456
+ return rest;
457
+ }
458
+ normalizeReviewedMetadataModel(reviewedModel, mappingDto) {
459
+ if (!reviewedModel) {
460
+ return null;
461
+ }
462
+ if (typeof reviewedModel !== "object" || Array.isArray(reviewedModel)) {
463
+ throw new common_1.BadRequestException("Reviewed metadata JSON must be a valid object.");
464
+ }
465
+ const readString = (key, allowEmpty = false) => {
466
+ const value = reviewedModel[key];
467
+ if (typeof value !== "string") {
468
+ throw new common_1.BadRequestException(`Reviewed metadata JSON is missing a valid "${key}" string value.`);
469
+ }
470
+ const trimmed = value.trim();
471
+ if (!allowEmpty && !trimmed) {
472
+ throw new common_1.BadRequestException(`Reviewed metadata JSON cannot leave "${key}" blank.`);
473
+ }
474
+ return trimmed;
475
+ };
476
+ const readBoolean = (key) => {
477
+ const value = reviewedModel[key];
478
+ if (typeof value !== "boolean") {
479
+ throw new common_1.BadRequestException(`Reviewed metadata JSON is missing a valid "${key}" boolean value.`);
480
+ }
481
+ return value;
482
+ };
483
+ const rawFields = reviewedModel.fields;
484
+ if (!Array.isArray(rawFields)) {
485
+ throw new common_1.BadRequestException("Reviewed metadata JSON must contain a fields array.");
486
+ }
487
+ const immutableKeyChecks = [
488
+ { key: "dataSource", label: "dataSource" },
489
+ { key: "dataSourceType", label: "dataSourceType" },
490
+ { key: "tableName", label: "tableName" },
491
+ { key: "legacyTableType", label: "legacyTableType" },
492
+ ];
493
+ for (const check of immutableKeyChecks) {
494
+ if (`${reviewedModel[check.label] ?? ""}` !== `${mappingDto[check.key] ?? ""}`) {
495
+ throw new common_1.BadRequestException(`Reviewed metadata JSON cannot change immutable binding key "${check.label}".`);
496
+ }
497
+ }
498
+ const normalizedFields = rawFields.map((field, index) => this.normalizeReviewedField(field, index));
499
+ const fieldNames = new Set();
500
+ for (const field of normalizedFields) {
501
+ const loweredName = field.name.toLowerCase();
502
+ if (fieldNames.has(loweredName)) {
503
+ throw new common_1.BadRequestException(`Reviewed metadata JSON contains duplicate field name "${field.name}".`);
504
+ }
505
+ fieldNames.add(loweredName);
506
+ }
507
+ const userKeyFieldUserKey = readString("userKeyFieldUserKey");
508
+ if (!normalizedFields.some((field) => field.name === userKeyFieldUserKey && !field.isMarkedForRemoval)) {
509
+ throw new common_1.BadRequestException("Reviewed metadata JSON must keep the selected userKeyFieldUserKey in the fields array.");
510
+ }
511
+ return {
512
+ singularName: readString("singularName"),
513
+ pluralName: readString("pluralName"),
514
+ displayName: readString("displayName"),
515
+ description: readString("description", true),
516
+ dataSource: readString("dataSource"),
517
+ dataSourceType: readString("dataSourceType"),
518
+ tableName: readString("tableName"),
519
+ userKeyFieldUserKey,
520
+ isChild: false,
521
+ legacyTableType: mappingDto.legacyTableType,
522
+ parentModelUserKey: typeof reviewedModel.parentModelUserKey === "string" ? reviewedModel.parentModelUserKey : "",
523
+ enableAuditTracking: readBoolean("enableAuditTracking"),
524
+ enableSoftDelete: readBoolean("enableSoftDelete"),
525
+ draftPublishWorkflow: readBoolean("draftPublishWorkflow"),
526
+ internationalisation: readBoolean("internationalisation"),
527
+ fields: normalizedFields.map((field) => ({
528
+ ...field,
529
+ isUserKey: field.name === userKeyFieldUserKey,
530
+ })),
531
+ };
532
+ }
533
+ normalizeReviewedField(field, index) {
534
+ if (!field || typeof field !== "object" || Array.isArray(field)) {
535
+ throw new common_1.BadRequestException(`Reviewed metadata JSON field at index ${index} is not a valid object.`);
536
+ }
537
+ if (typeof field.name !== "string" || !field.name.trim()) {
538
+ throw new common_1.BadRequestException(`Reviewed metadata JSON field at index ${index} must include a non-empty "name".`);
539
+ }
540
+ if (typeof field.type !== "string" || !field.type.trim()) {
541
+ throw new common_1.BadRequestException(`Reviewed metadata JSON field "${field.name}" must include a non-empty "type".`);
542
+ }
543
+ if (typeof field.ormType !== "string" || !field.ormType.trim()) {
544
+ throw new common_1.BadRequestException(`Reviewed metadata JSON field "${field.name}" must include a non-empty "ormType".`);
545
+ }
546
+ if (typeof field.columnName !== "string" || !field.columnName.trim()) {
547
+ throw new common_1.BadRequestException(`Reviewed metadata JSON field "${field.name}" must include a non-empty "columnName".`);
548
+ }
549
+ return {
550
+ ...field,
551
+ name: field.name.trim(),
552
+ type: field.type.trim(),
553
+ ormType: field.ormType.trim(),
554
+ columnName: field.columnName.trim(),
555
+ displayName: typeof field.displayName === "string" && field.displayName.trim()
556
+ ? field.displayName.trim()
557
+ : (0, lodash_1.startCase)(field.name),
558
+ description: typeof field.description === "string" ? field.description : "",
559
+ };
560
+ }
561
+ matchExistingFieldForReviewedModel(reviewedField, mappingDto, existingModel) {
562
+ const nonSystemFields = existingModel?.fields?.filter((field) => !field.isSystem) ?? [];
563
+ const byColumnName = typeof reviewedField.columnName === "string"
564
+ ? mappingDto.columns.find((column) => column.columnName.toLowerCase() === reviewedField.columnName.toLowerCase())
565
+ : null;
566
+ if (byColumnName?.fieldId) {
567
+ return nonSystemFields.find((field) => field.id === byColumnName.fieldId) ?? null;
568
+ }
569
+ const byColumnOnExisting = typeof reviewedField.columnName === "string"
570
+ ? nonSystemFields.find((field) => `${field.columnName ?? ""}`.toLowerCase() === reviewedField.columnName.toLowerCase())
571
+ : null;
572
+ if (byColumnOnExisting) {
573
+ return byColumnOnExisting;
574
+ }
575
+ const byName = nonSystemFields.find((field) => field.name?.toLowerCase?.() === reviewedField.name.toLowerCase());
576
+ return byName ?? null;
577
+ }
578
+ buildFieldPayload(column, mappingDto, existingField) {
579
+ const preserved = existingField ? this.sanitizeExistingFieldPayload(existingField) : {};
580
+ const maxLength = column.characterMaximumLength && ["shortText", "longText"].includes(column.solidFieldType)
581
+ ? column.characterMaximumLength
582
+ : preserved.max;
583
+ return {
584
+ ...preserved,
585
+ ...(column.fieldId ? { id: column.fieldId } : {}),
586
+ name: column.fieldName,
587
+ displayName: preserved.displayName ?? (0, lodash_1.startCase)(column.fieldName),
588
+ description: preserved.description ?? "",
589
+ type: column.solidFieldType,
590
+ ormType: column.ormType,
591
+ length: preserved.length ?? column.characterMaximumLength ?? undefined,
592
+ defaultValue: preserved.defaultValue ?? column.defaultValue ?? undefined,
593
+ required: preserved.required ?? (!column.nullable || column.isPrimaryKey),
594
+ unique: preserved.unique ?? false,
595
+ encrypt: preserved.encrypt ?? false,
596
+ index: preserved.index ?? false,
597
+ max: maxLength ?? undefined,
598
+ min: preserved.min ?? undefined,
599
+ private: preserved.private ?? false,
600
+ isSystem: preserved.isSystem ?? false,
601
+ isMarkedForRemoval: false,
602
+ columnName: column.columnName,
603
+ isUserKey: mappingDto.userKeyField === column.fieldName,
604
+ enableAuditTracking: preserved.enableAuditTracking ?? mappingDto.enableAuditTracking,
605
+ isMultiSelect: preserved.isMultiSelect ?? false,
606
+ isPrimaryKey: column.isPrimaryKey,
607
+ };
608
+ }
609
+ toFieldPreviewKey(id, name) {
610
+ return typeof id === "number" ? `id:${id}` : `name:${name.toLowerCase()}`;
611
+ }
612
+ getLegacySystemColumnDefinitions(legacyTableType, datasourceType) {
613
+ const normalizedDatasourceType = datasourceType.toLowerCase();
614
+ const timestampType = normalizedDatasourceType === "postgres"
615
+ ? "timestamp"
616
+ : normalizedDatasourceType === "mysql"
617
+ ? "datetime"
618
+ : "datetime2";
619
+ const publishedAtType = normalizedDatasourceType === "postgres"
620
+ ? "timestamp"
621
+ : "datetime";
622
+ const integerType = normalizedDatasourceType === "postgres" ? "integer" : "int";
623
+ const varcharType = normalizedDatasourceType === "mssql" ? "nvarchar" : "varchar";
624
+ const currentTimestampDefault = normalizedDatasourceType === "postgres"
625
+ ? "now()"
626
+ : normalizedDatasourceType === "mysql"
627
+ ? "CURRENT_TIMESTAMP"
628
+ : "getdate()";
629
+ return this.modelMetadataHelperService
630
+ .getSystemFieldsMetadata(legacyTableType)
631
+ .map((field) => {
632
+ switch (field.name) {
633
+ case "id":
634
+ return {
635
+ columnName: field.columnName,
636
+ type: integerType,
637
+ isNullable: false,
638
+ isGenerated: true,
639
+ isUnique: true,
640
+ };
641
+ case "createdAt":
642
+ case "updatedAt":
643
+ return {
644
+ columnName: field.columnName,
645
+ type: timestampType,
646
+ isNullable: false,
647
+ defaultExpression: currentTimestampDefault,
648
+ };
649
+ case "deletedAt":
650
+ return {
651
+ columnName: field.columnName,
652
+ type: timestampType,
653
+ isNullable: true,
654
+ };
655
+ case "deletedTracker":
656
+ return {
657
+ columnName: field.columnName,
658
+ type: varcharType,
659
+ length: "255",
660
+ isNullable: false,
661
+ defaultExpression: "'not-deleted'",
662
+ };
663
+ case "publishedAt":
664
+ return {
665
+ columnName: field.columnName,
666
+ type: publishedAtType,
667
+ isNullable: true,
668
+ };
669
+ case "localeName":
670
+ return {
671
+ columnName: field.columnName,
672
+ type: "varchar",
673
+ length: "255",
674
+ isNullable: true,
675
+ };
676
+ case "defaultEntityLocaleId":
677
+ case "createdBy":
678
+ case "updatedBy":
679
+ return {
680
+ columnName: field.columnName,
681
+ type: integerType,
682
+ isNullable: true,
683
+ };
684
+ default:
685
+ return {
686
+ columnName: field.columnName,
687
+ type: "varchar",
688
+ isNullable: true,
689
+ };
690
+ }
691
+ });
692
+ }
693
+ renderMigrationTemplate(className, mappingDto, missingSystemColumns) {
694
+ const tableNameLiteral = this.toMigrationTableName(mappingDto.schema, mappingDto.tableName);
695
+ const deletedAtIndexName = this.buildMigrationObjectName("IDX", tableNameLiteral, "ss_deleted_at");
696
+ const generatedIdUniqueName = this.buildMigrationObjectName("UQ", tableNameLiteral, "ss_id");
697
+ const upColumnStatements = missingSystemColumns.map((definition) => {
698
+ const columnLiteral = this.renderTableColumnLiteral(definition);
699
+ return [
700
+ ` if (!(await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)}))) {`,
701
+ ` await queryRunner.addColumn(tableName, new TableColumn(${columnLiteral}));`,
702
+ " }",
703
+ ].join("\n");
704
+ }).join("\n\n");
705
+ const postUpStatements = [];
706
+ if (missingSystemColumns.some((definition) => definition.columnName === "ss_deleted_at")) {
707
+ postUpStatements.push([
708
+ " {",
709
+ " const table = await queryRunner.getTable(tableName);",
710
+ ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(deletedAtIndexName)})) {`,
711
+ " await queryRunner.createIndex(tableName, new TableIndex({",
712
+ ` name: ${JSON.stringify(deletedAtIndexName)},`,
713
+ " columnNames: [\"ss_deleted_at\"],",
714
+ " }));",
715
+ " }",
716
+ " }",
717
+ ].join("\n"));
718
+ }
719
+ if (missingSystemColumns.some((definition) => definition.columnName === "ss_id")) {
720
+ postUpStatements.push([
721
+ " {",
722
+ " const table = await queryRunner.getTable(tableName);",
723
+ ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(generatedIdUniqueName)})) {`,
724
+ " await queryRunner.createIndex(tableName, new TableIndex({",
725
+ ` name: ${JSON.stringify(generatedIdUniqueName)},`,
726
+ " columnNames: [\"ss_id\"],",
727
+ " isUnique: true,",
728
+ " }));",
729
+ " }",
730
+ " }",
731
+ ].join("\n"));
732
+ }
733
+ const downStatements = [];
734
+ if (missingSystemColumns.some((definition) => definition.columnName === "ss_id")) {
735
+ downStatements.push([
736
+ " {",
737
+ " const table = await queryRunner.getTable(tableName);",
738
+ ` const index = table?.indices.find((entry) => entry.name === ${JSON.stringify(generatedIdUniqueName)});`,
739
+ " if (index) {",
740
+ " await queryRunner.dropIndex(tableName, index);",
741
+ " }",
742
+ " }",
743
+ ].join("\n"));
744
+ }
745
+ if (missingSystemColumns.some((definition) => definition.columnName === "ss_deleted_at")) {
746
+ downStatements.push([
747
+ " {",
748
+ " const table = await queryRunner.getTable(tableName);",
749
+ ` const index = table?.indices.find((entry) => entry.name === ${JSON.stringify(deletedAtIndexName)});`,
750
+ " if (index) {",
751
+ " await queryRunner.dropIndex(tableName, index);",
752
+ " }",
753
+ " }",
754
+ ].join("\n"));
755
+ }
756
+ downStatements.push(...missingSystemColumns
757
+ .slice()
758
+ .reverse()
759
+ .map((definition) => [
760
+ ` if (await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)})) {`,
761
+ ` await queryRunner.dropColumn(tableName, ${JSON.stringify(definition.columnName)});`,
762
+ " }",
763
+ ].join("\n")));
764
+ return [
765
+ "import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from \"typeorm\";",
766
+ "",
767
+ `export class ${className} implements MigrationInterface {`,
768
+ ` name = '${className}'`,
769
+ "",
770
+ " public async up(queryRunner: QueryRunner): Promise<void> {",
771
+ ` const tableName = ${JSON.stringify(tableNameLiteral)};`,
772
+ upColumnStatements,
773
+ postUpStatements.length ? `\n${postUpStatements.join("\n\n")}` : "",
774
+ " }",
775
+ "",
776
+ " public async down(queryRunner: QueryRunner): Promise<void> {",
777
+ ` const tableName = ${JSON.stringify(tableNameLiteral)};`,
778
+ downStatements.join("\n\n"),
779
+ " }",
780
+ "}",
781
+ "",
782
+ ].filter(Boolean).join("\n");
783
+ }
784
+ renderTableColumnLiteral(definition) {
785
+ const properties = [
786
+ `{\n name: ${JSON.stringify(definition.columnName)},`,
787
+ ` type: ${JSON.stringify(definition.type)},`,
788
+ ];
789
+ if (definition.length) {
790
+ properties.push(` length: ${JSON.stringify(definition.length)},`);
791
+ }
792
+ if (typeof definition.isNullable === "boolean") {
793
+ properties.push(` isNullable: ${definition.isNullable},`);
794
+ }
795
+ if (definition.defaultExpression) {
796
+ properties.push(` default: ${JSON.stringify(definition.defaultExpression)},`);
797
+ }
798
+ if (definition.isGenerated) {
799
+ properties.push(" isGenerated: true,");
800
+ properties.push(" generationStrategy: \"increment\",");
801
+ }
802
+ if (definition.isUnique) {
803
+ properties.push(" isUnique: true,");
804
+ }
805
+ properties.push(" }");
806
+ return properties.join("\n");
807
+ }
808
+ buildMigrationObjectName(prefix, tableName, columnName) {
809
+ const hash = (0, crypto_1.createHash)("md5").update(`${tableName}:${columnName}:${prefix}`).digest("hex").slice(0, 24);
810
+ return `${prefix}_${hash}`;
811
+ }
812
+ toMigrationTableName(schema, tableName) {
813
+ return schema ? `${schema}.${tableName}` : tableName;
814
+ }
815
+ async buildRawDdl(provider, dataSource, table) {
816
+ try {
817
+ const statement = await provider.getTableDefinition(dataSource, table);
818
+ return {
819
+ statement,
820
+ available: Boolean(statement),
821
+ note: statement ? null : "The datasource provider did not return a CREATE TABLE statement for this table.",
822
+ };
823
+ }
824
+ catch (error) {
825
+ return {
826
+ statement: null,
827
+ available: false,
828
+ note: `DDL preview is unavailable right now: ${error?.message ?? "Unknown error."}`,
829
+ };
830
+ }
831
+ }
832
+ async buildTableInventory(datasource) {
833
+ const provider = this.getProvider(datasource.type);
834
+ const runtimeDataSource = this.resolveDataSourceByName(datasource.name);
835
+ const [tables, columns, models] = await Promise.all([
836
+ provider.listTables(runtimeDataSource),
837
+ provider.listColumns(runtimeDataSource),
838
+ this.modelMetadataRepository.find({ relations: { module: true } }),
839
+ ]);
840
+ const modelMap = this.buildModelMap(models);
841
+ const columnsByKey = this.groupColumnsByTable(columns);
842
+ const tableKeys = new Set();
843
+ for (const table of tables) {
844
+ tableKeys.add(this.toTableKey(table.tableSchema, table.tableName));
845
+ }
846
+ for (const column of columns) {
847
+ tableKeys.add(this.toTableKey(column.tableSchema, column.tableName));
848
+ }
849
+ const records = Array.from(tableKeys)
850
+ .map((tableKey) => {
851
+ const matchingTable = tables.find((table) => this.toTableKey(table.tableSchema, table.tableName) === tableKey);
852
+ const tableColumns = columnsByKey.get(tableKey) ?? [];
853
+ const schema = matchingTable?.tableSchema ?? tableColumns[0]?.tableSchema ?? null;
854
+ const tableName = matchingTable?.tableName ?? tableColumns[0]?.tableName;
855
+ if (!tableName) {
856
+ return null;
857
+ }
858
+ const suggestedLegacyTableType = this.detectLegacyTableType(datasource.type, tableColumns);
859
+ const suggestedIdColumn = this.getSuggestedIdColumn(datasource.type, tableColumns, suggestedLegacyTableType);
860
+ const mappedModel = modelMap.get(this.toModelMapKey(datasource.name, tableName)) ?? null;
861
+ const physicalColumns = tableColumns.filter((column) => !this.isHandledBySuperclass(column.columnName, suggestedLegacyTableType));
862
+ return {
863
+ schema,
864
+ tableName,
865
+ mapped: Boolean(mappedModel),
866
+ mappedModelId: mappedModel?.id ?? null,
867
+ mappedModelSingularName: mappedModel?.singularName ?? null,
868
+ mappedModelDisplayName: mappedModel?.displayName ?? null,
869
+ mappedModuleId: mappedModel?.module?.id ?? null,
870
+ mappedModuleName: mappedModel?.module?.name ?? null,
871
+ mappedModuleDisplayName: mappedModel?.module?.displayName ?? null,
872
+ suggestedLegacyTableType,
873
+ suggestedBaseClassName: this.getBaseClassName(suggestedLegacyTableType),
874
+ suggestedIdColumnName: suggestedIdColumn?.columnName ?? null,
875
+ suggestedUserKeyField: this.buildUserKeyCandidates(tableColumns)[0]?.name ?? "id",
876
+ columnCount: tableColumns.length,
877
+ physicalColumnCount: physicalColumns.length,
878
+ };
879
+ })
880
+ .filter((record) => Boolean(record))
881
+ .sort((left, right) => {
882
+ if (left.mapped !== right.mapped) {
883
+ return left.mapped ? 1 : -1;
884
+ }
885
+ const leftLabel = `${left.schema ?? ""}.${left.tableName}`.toLowerCase();
886
+ const rightLabel = `${right.schema ?? ""}.${right.tableName}`.toLowerCase();
887
+ return leftLabel.localeCompare(rightLabel);
888
+ });
889
+ return {
890
+ records,
891
+ recordsByKey: new Map(records.map((record) => [this.toTableKey(record.schema, record.tableName), record])),
892
+ columnsByKey,
893
+ };
894
+ }
895
+ buildModelMap(models) {
896
+ const map = new Map();
897
+ for (const model of models) {
898
+ if (!model.tableName) {
899
+ continue;
900
+ }
901
+ map.set(this.toModelMapKey(model.dataSource, model.tableName), model);
902
+ map.set(this.toModelMapKey("*", model.tableName), model);
903
+ }
904
+ return map;
905
+ }
906
+ groupColumnsByTable(columns) {
907
+ const grouped = new Map();
908
+ for (const column of columns) {
909
+ const key = this.toTableKey(column.tableSchema, column.tableName);
910
+ const bucket = grouped.get(key);
911
+ if (bucket) {
912
+ bucket.push(column);
913
+ continue;
914
+ }
915
+ grouped.set(key, [column]);
916
+ }
917
+ return grouped;
918
+ }
919
+ detectLegacyTableType(datasourceType, columns) {
920
+ const normalizedDatasourceType = datasourceType.toLowerCase();
921
+ const hasExistingGeneratedId = columns.some((column) => column.isAutoGenerated);
922
+ if ((normalizedDatasourceType === "mssql" || normalizedDatasourceType === "mysql") && hasExistingGeneratedId) {
923
+ return legacy_table_type_enum_1.LegacyTableType.EXISTING_ID;
924
+ }
925
+ return legacy_table_type_enum_1.LegacyTableType.GENERATED_ID;
926
+ }
927
+ getSuggestedIdColumn(datasourceType, columns, legacyTableType) {
928
+ if (legacyTableType !== legacy_table_type_enum_1.LegacyTableType.EXISTING_ID) {
929
+ return null;
930
+ }
931
+ const normalizedDatasourceType = datasourceType.toLowerCase();
932
+ if (normalizedDatasourceType !== "mssql" && normalizedDatasourceType !== "mysql") {
933
+ return null;
934
+ }
935
+ return columns.find((column) => column.isAutoGenerated) ?? null;
936
+ }
937
+ buildUserKeyCandidates(columns) {
938
+ const eligibleColumns = columns.filter((column) => !column.columnName.toLowerCase().startsWith("ss_"));
939
+ const scoreMap = new Map();
940
+ const priorityMatchers = [
941
+ { score: 100, reason: "Exact legacy external identifier", test: (name) => name === "external_id" || name === "externalid" },
942
+ { score: 95, reason: "Exact user key style column", test: (name) => name === "user_key" || name === "userkey" },
943
+ { score: 90, reason: "Code-style business identifier", test: (name) => name === "code" || name.endsWith("_code") },
944
+ { score: 85, reason: "Reference-style business identifier", test: (name) => name.includes("reference") || name.endsWith("_number") || name.endsWith("_no") },
945
+ { score: 80, reason: "Human readable display field", test: (name) => name === "name" || name === "display_name" || name.endsWith("_name") },
946
+ { score: 70, reason: "Contact field that could identify a record", test: (name) => name === "email" || name === "mobile" || name === "phone" },
947
+ { score: 60, reason: "Fallback primary identifier", test: (name) => name === "id" },
948
+ ];
949
+ for (const column of eligibleColumns) {
950
+ const normalizedName = column.columnName.toLowerCase();
951
+ for (const matcher of priorityMatchers) {
952
+ if (!matcher.test(normalizedName)) {
953
+ continue;
954
+ }
955
+ const existing = scoreMap.get(column.columnName);
956
+ if (!existing || matcher.score > existing.score) {
957
+ scoreMap.set(column.columnName, {
958
+ name: this.toFieldName(column.columnName),
959
+ label: column.columnName,
960
+ score: matcher.score,
961
+ reason: matcher.reason,
962
+ });
963
+ }
964
+ }
965
+ }
966
+ const candidates = Array.from(scoreMap.values())
967
+ .sort((left, right) => right.score - left.score || left.label.localeCompare(right.label))
968
+ .map((candidate, index) => ({
969
+ ...candidate,
970
+ isDefault: index === 0,
971
+ }));
972
+ if (candidates.length) {
973
+ return candidates;
974
+ }
975
+ const hasIdColumn = eligibleColumns.some((column) => column.columnName.toLowerCase() === "id");
976
+ return [{
977
+ name: "id",
978
+ label: hasIdColumn ? "id" : "id (fallback)",
979
+ score: 0,
980
+ reason: "Default to the SolidX id field when no stronger user key candidate is available.",
981
+ isDefault: true,
982
+ }];
983
+ }
984
+ toDetailColumnRecord(column, legacyTableType, datasourceType) {
985
+ const superclassFieldName = this.getSuperclassFieldName(column.columnName, legacyTableType);
986
+ const fieldName = legacyTableType === legacy_table_type_enum_1.LegacyTableType.EXISTING_ID && column.isAutoGenerated
987
+ ? "id"
988
+ : this.toFieldName(column.columnName);
989
+ return {
990
+ columnName: column.columnName,
991
+ fieldName,
992
+ dataType: column.dataType,
993
+ solidFieldType: this.toSolidFieldType(column),
994
+ ormType: this.toOrmFieldType(column, datasourceType),
995
+ nullable: column.isNullable,
996
+ defaultValue: column.defaultValue,
997
+ characterMaximumLength: column.characterMaximumLength,
998
+ numericPrecision: column.numericPrecision,
999
+ numericScale: column.numericScale,
1000
+ ordinalPosition: column.ordinalPosition,
1001
+ isAutoGenerated: column.isAutoGenerated,
1002
+ handledBySuperclass: Boolean(superclassFieldName),
1003
+ superclassFieldName,
1004
+ };
1005
+ }
1006
+ getSuperclassFieldName(columnName, legacyTableType) {
1007
+ return this.getLegacySuperclassFieldName(columnName, legacyTableType);
1008
+ }
1009
+ getLegacySuperclassFieldName(columnName, legacyTableType) {
1010
+ const normalized = columnName.toLowerCase();
1011
+ const legacyFieldMap = {
1012
+ ss_created_at: "createdAt",
1013
+ ss_updated_at: "updatedAt",
1014
+ ss_deleted_at: "deletedAt",
1015
+ ss_deleted_tracker: "deletedTracker",
1016
+ ss_published_at: "publishedAt",
1017
+ ss_locale_name: "localeName",
1018
+ ss_default_entity_locale_id: "defaultEntityLocaleId",
1019
+ ss_created_by_id: "createdBy",
1020
+ ss_updated_by_id: "updatedBy",
1021
+ };
1022
+ if (normalized === "ss_id" && legacyTableType === legacy_table_type_enum_1.LegacyTableType.GENERATED_ID) {
1023
+ return "id";
1024
+ }
1025
+ return legacyFieldMap[normalized] ?? null;
1026
+ }
1027
+ isHandledBySuperclass(columnName, legacyTableType) {
1028
+ return Boolean(this.getSuperclassFieldName(columnName, legacyTableType));
1029
+ }
1030
+ getBaseClassName(legacyTableType) {
1031
+ if (legacyTableType === legacy_table_type_enum_1.LegacyTableType.GENERATED_ID) {
1032
+ return "LegacyCommonEntityWithGeneratedId";
1033
+ }
1034
+ return "LegacyCommonEntityWithExistingId";
1035
+ }
1036
+ toSolidFieldType(column) {
1037
+ const normalized = column.dataType.toLowerCase();
1038
+ const charLength = column.characterMaximumLength ?? null;
1039
+ if (["bit", "bool", "boolean"].includes(normalized)) {
1040
+ return "boolean";
1041
+ }
1042
+ if (["bigint"].includes(normalized)) {
1043
+ return "bigint";
1044
+ }
1045
+ if (["int", "integer", "smallint", "tinyint"].includes(normalized)) {
1046
+ return "int";
1047
+ }
1048
+ if (["numeric", "decimal", "float", "double", "real", "money", "smallmoney"].includes(normalized)) {
1049
+ return "decimal";
1050
+ }
1051
+ if (["date"].includes(normalized)) {
1052
+ return "date";
1053
+ }
1054
+ if (["time"].includes(normalized)) {
1055
+ return "time";
1056
+ }
1057
+ if (["datetime", "datetime2", "timestamp", "timestamptz", "smalldatetime"].includes(normalized) || normalized.includes("date")) {
1058
+ return "datetime";
1059
+ }
1060
+ if (["json", "jsonb", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json")) {
1061
+ return "json";
1062
+ }
1063
+ if (["uniqueidentifier", "uuid"].includes(normalized)) {
1064
+ return "uuid";
1065
+ }
1066
+ if (["text", "ntext", "mediumtext", "longtext", "clob"].includes(normalized)) {
1067
+ return "longText";
1068
+ }
1069
+ if (["varchar", "nvarchar", "char", "nchar", "citext"].includes(normalized)) {
1070
+ if (charLength !== null && charLength > 255) {
1071
+ return "longText";
1072
+ }
1073
+ return "shortText";
1074
+ }
1075
+ return "shortText";
1076
+ }
1077
+ toOrmFieldType(column, datasourceType) {
1078
+ const normalized = column.dataType.toLowerCase();
1079
+ const normalizedDatasourceType = datasourceType.toLowerCase();
1080
+ if (normalizedDatasourceType === "mssql") {
1081
+ if (["bit", "bool", "boolean"].includes(normalized))
1082
+ return "bit";
1083
+ if (["bigint"].includes(normalized))
1084
+ return "bigint";
1085
+ if (["int", "integer", "smallint", "tinyint"].includes(normalized))
1086
+ return "int";
1087
+ if (["numeric"].includes(normalized))
1088
+ return "numeric";
1089
+ if (["decimal", "float", "double", "real", "money", "smallmoney"].includes(normalized))
1090
+ return "decimal";
1091
+ if (["date"].includes(normalized))
1092
+ return "date";
1093
+ if (["time"].includes(normalized))
1094
+ return "time";
1095
+ if (["datetime2"].includes(normalized))
1096
+ return "datetime2";
1097
+ if (["datetime", "smalldatetime", "timestamp"].includes(normalized) || normalized.includes("date"))
1098
+ return "datetime";
1099
+ if (["json", "jsonb", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json"))
1100
+ return "simple-json";
1101
+ if (["uniqueidentifier", "uuid"].includes(normalized))
1102
+ return "uniqueidentifier";
1103
+ if (["text", "ntext", "mediumtext", "longtext", "clob"].includes(normalized))
1104
+ return "text";
1105
+ if (["nvarchar", "nchar"].includes(normalized))
1106
+ return "nvarchar";
1107
+ return "varchar";
1108
+ }
1109
+ if (normalizedDatasourceType === "mysql") {
1110
+ if (["bigint"].includes(normalized))
1111
+ return "int";
1112
+ if (["int", "integer", "smallint", "tinyint"].includes(normalized))
1113
+ return "int";
1114
+ if (["decimal", "numeric"].includes(normalized))
1115
+ return "decimal";
1116
+ if (["float", "double", "real", "money", "smallmoney"].includes(normalized))
1117
+ return "double";
1118
+ if (["bit", "bool", "boolean"].includes(normalized))
1119
+ return "boolean";
1120
+ if (["date"].includes(normalized))
1121
+ return "date";
1122
+ if (["time"].includes(normalized))
1123
+ return "time";
1124
+ if (["datetime", "timestamp", "smalldatetime"].includes(normalized) || normalized.includes("date"))
1125
+ return "datetime";
1126
+ if (["json", "jsonb", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json"))
1127
+ return "json";
1128
+ if (["mediumtext"].includes(normalized))
1129
+ return "mediumtext";
1130
+ if (["longtext", "text", "ntext", "clob"].includes(normalized))
1131
+ return "longtext";
1132
+ return "varchar";
1133
+ }
1134
+ if (["bigint"].includes(normalized))
1135
+ return "bigint";
1136
+ if (["int", "integer", "smallint", "tinyint"].includes(normalized))
1137
+ return "integer";
1138
+ if (["decimal", "numeric", "float", "double", "real", "money", "smallmoney"].includes(normalized))
1139
+ return "decimal";
1140
+ if (["bit", "bool", "boolean"].includes(normalized))
1141
+ return "boolean";
1142
+ if (["date"].includes(normalized))
1143
+ return "date";
1144
+ if (["time"].includes(normalized))
1145
+ return "time";
1146
+ if (["timestamptz"].includes(normalized))
1147
+ return "timestamptz";
1148
+ if (["datetime", "datetime2", "timestamp", "smalldatetime"].includes(normalized) || normalized.includes("date"))
1149
+ return "timestamp";
1150
+ if (["jsonb"].includes(normalized))
1151
+ return "jsonb";
1152
+ if (["json", "simple-json", "simplejson"].includes(normalized) || normalized.includes("json"))
1153
+ return "json";
1154
+ if (["text", "ntext", "mediumtext", "longtext", "clob"].includes(normalized))
1155
+ return "text";
1156
+ return "varchar";
1157
+ }
1158
+ toFieldName(columnName) {
1159
+ return (0, lodash_1.camelCase)(columnName.replace(/^ss_/, ""));
1160
+ }
1161
+ toSingularNameCandidate(tableName) {
1162
+ const normalized = (0, lodash_1.camelCase)(tableName);
1163
+ if (normalized.endsWith("ies")) {
1164
+ return `${normalized.slice(0, -3)}y`;
1165
+ }
1166
+ if (normalized.endsWith("ses")) {
1167
+ return normalized.slice(0, -2);
1168
+ }
1169
+ if (normalized.endsWith("s") && !normalized.endsWith("ss")) {
1170
+ return normalized.slice(0, -1);
1171
+ }
1172
+ return normalized;
1173
+ }
1174
+ toPluralNameCandidate(tableName, singularName) {
1175
+ const normalized = (0, lodash_1.camelCase)(tableName);
1176
+ if (normalized && normalized !== singularName) {
1177
+ return normalized;
1178
+ }
1179
+ if (singularName.endsWith("y")) {
1180
+ return `${singularName.slice(0, -1)}ies`;
1181
+ }
1182
+ if (singularName.endsWith("s")) {
1183
+ return `${singularName}es`;
1184
+ }
1185
+ return `${singularName}s`;
1186
+ }
1187
+ toTableKey(schema, tableName) {
1188
+ return `${(schema ?? "").toLowerCase()}::${tableName.toLowerCase()}`;
1189
+ }
1190
+ toModelMapKey(datasourceName, tableName) {
1191
+ return `${datasourceName.toLowerCase()}::${tableName.toLowerCase()}`;
1192
+ }
1193
+ buildSynchronizeBlockedMessage(datasource) {
1194
+ return `Datasource "${datasource.displayName}" has synchronize enabled. Set ${datasource.envPrefix}_DATABASE_SYNCHRONIZE=false before starting legacy table introspection.`;
1195
+ }
1196
+ resolveDataSourceByName(datasourceName) {
1197
+ const token = datasourceName === "default" ? (0, typeorm_1.getDataSourceToken)() : (0, typeorm_1.getDataSourceToken)(datasourceName);
1198
+ try {
1199
+ const dataSource = this.moduleRef.get(token, { strict: false });
1200
+ if (!dataSource) {
1201
+ throw new common_1.NotFoundException(`Datasource "${datasourceName}" is not registered in the current runtime.`);
1202
+ }
1203
+ return dataSource;
1204
+ }
1205
+ catch (error) {
1206
+ throw new common_1.NotFoundException(`Failed to resolve datasource "${datasourceName}": ${error?.message ?? error}`);
1207
+ }
1208
+ }
1209
+ getProvider(type) {
1210
+ const providers = [
1211
+ this.mssqlProvider,
1212
+ this.mysqlProvider,
1213
+ this.postgresProvider,
1214
+ ];
1215
+ const provider = providers.find((candidate) => candidate.supports(type));
1216
+ if (!provider) {
1217
+ throw new common_1.BadRequestException(`Datasource type "${type}" is not supported for introspection yet.`);
1218
+ }
1219
+ return provider;
1220
+ }
1221
+ requireDatasource(datasourceName) {
1222
+ const datasource = this.datasourceManagementService.findOneByName(datasourceName);
1223
+ if (!datasource) {
1224
+ throw new common_1.NotFoundException(`Datasource "${datasourceName}" is not configured.`);
1225
+ }
1226
+ if (!this.supportedDatasourceTypes.has(datasource.type?.toLowerCase?.() ?? "")) {
1227
+ throw new common_1.BadRequestException(`Datasource "${datasource.type}" is not supported for introspection yet.`);
1228
+ }
1229
+ return datasource;
1230
+ }
1231
+ async loadModule(moduleId) {
1232
+ const module = await this.moduleMetadataRepository.findOne({
1233
+ where: { id: moduleId },
1234
+ });
1235
+ if (!module) {
1236
+ throw new common_1.NotFoundException(`Module with id "${moduleId}" was not found.`);
1237
+ }
1238
+ return module;
1239
+ }
1240
+ toModuleSummary(module) {
1241
+ return {
1242
+ id: module.id,
1243
+ name: module.name,
1244
+ displayName: module.displayName,
1245
+ description: module.description ?? "",
1246
+ defaultDataSource: module.defaultDataSource ?? "",
1247
+ };
1248
+ }
1249
+ toDatasourceSummary(record) {
1250
+ return {
1251
+ name: record.name,
1252
+ displayName: record.displayName,
1253
+ type: record.type,
1254
+ envPrefix: record.envPrefix,
1255
+ isDefault: record.isDefault,
1256
+ synchronize: record.synchronize,
1257
+ host: record.host,
1258
+ port: record.port,
1259
+ database: record.database,
1260
+ };
1261
+ }
1262
+ };
1263
+ exports.DatasourceIntrospectionService = DatasourceIntrospectionService;
1264
+ exports.DatasourceIntrospectionService = DatasourceIntrospectionService = __decorate([
1265
+ (0, common_1.Injectable)(),
1266
+ __metadata("design:paramtypes", [core_1.ModuleRef,
1267
+ datasource_management_service_1.DatasourceManagementService,
1268
+ module_metadata_repository_1.ModuleMetadataRepository,
1269
+ model_metadata_repository_1.ModelMetadataRepository,
1270
+ module_metadata_helper_service_1.ModuleMetadataHelperService,
1271
+ model_metadata_helper_service_1.ModelMetadataHelperService,
1272
+ field_metadata_service_1.FieldMetadataService,
1273
+ model_metadata_service_1.ModelMetadataService,
1274
+ mssql_datasource_introspection_provider_service_1.MssqlDatasourceIntrospectionProviderService,
1275
+ mysql_datasource_introspection_provider_service_1.MysqlDatasourceIntrospectionProviderService,
1276
+ postgres_datasource_introspection_provider_service_1.PostgresDatasourceIntrospectionProviderService])
1277
+ ], DatasourceIntrospectionService);
1278
+ //# sourceMappingURL=datasource-introspection.service.js.map