@solidxai/core 0.1.11-beta.1 → 0.1.11-beta.12

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 (221) 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 +46 -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/controllers/service.controller.d.ts +1 -0
  10. package/dist/controllers/service.controller.d.ts.map +1 -1
  11. package/dist/controllers/service.controller.js +2 -2
  12. package/dist/controllers/service.controller.js.map +1 -1
  13. package/dist/dtos/datasource-introspection-mapping.dto.d.ts +5 -0
  14. package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -1
  15. package/dist/dtos/datasource-introspection-mapping.dto.js +24 -2
  16. package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -1
  17. package/dist/entities/{legacy-common-with-id.entity.d.ts → legacy-common-with-generated-id.entity.d.ts} +2 -2
  18. package/dist/entities/legacy-common-with-generated-id.entity.d.ts.map +1 -0
  19. package/dist/entities/{legacy-common-with-id.entity.js → legacy-common-with-generated-id.entity.js} +4 -4
  20. package/dist/entities/legacy-common-with-generated-id.entity.js.map +1 -0
  21. package/dist/entities/{legacy-common.entity.d.ts → legacy-common.entity-with-existing-id.entity.d.ts} +1 -1
  22. package/dist/entities/legacy-common.entity-with-existing-id.entity.d.ts.map +1 -0
  23. package/dist/entities/{legacy-common.entity.js → legacy-common.entity-with-existing-id.entity.js} +1 -1
  24. package/dist/entities/legacy-common.entity-with-existing-id.entity.js.map +1 -0
  25. package/dist/helpers/command.service.d.ts +2 -0
  26. package/dist/helpers/command.service.d.ts.map +1 -1
  27. package/dist/helpers/command.service.js +6 -2
  28. package/dist/helpers/command.service.js.map +1 -1
  29. package/dist/helpers/environment.helper.d.ts +1 -0
  30. package/dist/helpers/environment.helper.d.ts.map +1 -1
  31. package/dist/helpers/environment.helper.js +4 -0
  32. package/dist/helpers/environment.helper.js.map +1 -1
  33. package/dist/helpers/field-crud-managers/IntFieldCrudManager.d.ts.map +1 -1
  34. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js +4 -1
  35. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js.map +1 -1
  36. package/dist/helpers/model-metadata-helper.service.d.ts +2 -1
  37. package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
  38. package/dist/helpers/model-metadata-helper.service.js +9 -5
  39. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  40. package/dist/helpers/module-metadata-helper.service.js +1 -1
  41. package/dist/helpers/module-metadata-helper.service.js.map +1 -1
  42. package/dist/helpers/nodemon-heartbeat.d.ts +5 -0
  43. package/dist/helpers/nodemon-heartbeat.d.ts.map +1 -0
  44. package/dist/helpers/nodemon-heartbeat.js +67 -0
  45. package/dist/helpers/nodemon-heartbeat.js.map +1 -0
  46. package/dist/helpers/solid-registry.d.ts +2 -1
  47. package/dist/helpers/solid-registry.d.ts.map +1 -1
  48. package/dist/helpers/solid-registry.js.map +1 -1
  49. package/dist/helpers/typeorm-db-helper.d.ts +1 -0
  50. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  51. package/dist/helpers/typeorm-db-helper.js +12 -0
  52. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  53. package/dist/index.d.ts +3 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/interfaces/datasource-introspection-provider.interface.d.ts +1 -0
  58. package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -1
  59. package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -1
  60. package/dist/interfaces.d.ts +1 -0
  61. package/dist/interfaces.d.ts.map +1 -1
  62. package/dist/interfaces.js.map +1 -1
  63. package/dist/repository/scheduled-job.repository.d.ts +2 -2
  64. package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
  65. package/dist/repository/scheduled-job.repository.js +6 -3
  66. package/dist/repository/scheduled-job.repository.js.map +1 -1
  67. package/dist/repository/security-rule.repository.d.ts +2 -2
  68. package/dist/repository/security-rule.repository.d.ts.map +1 -1
  69. package/dist/repository/security-rule.repository.js +6 -3
  70. package/dist/repository/security-rule.repository.js.map +1 -1
  71. package/dist/seeders/module-metadata-seeder.service.d.ts +11 -1
  72. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  73. package/dist/seeders/module-metadata-seeder.service.js +163 -3
  74. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  75. package/dist/seeders/seed-data/solid-core-metadata.json +128 -34
  76. package/dist/services/api-key.service.d.ts.map +1 -1
  77. package/dist/services/api-key.service.js.map +1 -1
  78. package/dist/services/authentication.service.d.ts +2 -0
  79. package/dist/services/authentication.service.d.ts.map +1 -1
  80. package/dist/services/authentication.service.js +32 -11
  81. package/dist/services/authentication.service.js.map +1 -1
  82. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
  83. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +7 -5
  84. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
  85. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +4 -0
  86. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -1
  87. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +18 -1
  88. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -1
  89. package/dist/services/crud-helper.service.d.ts.map +1 -1
  90. package/dist/services/crud-helper.service.js +25 -12
  91. package/dist/services/crud-helper.service.js.map +1 -1
  92. package/dist/services/crud.service.d.ts +1 -0
  93. package/dist/services/crud.service.d.ts.map +1 -1
  94. package/dist/services/crud.service.js +27 -0
  95. package/dist/services/crud.service.js.map +1 -1
  96. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -1
  97. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +12 -0
  98. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -1
  99. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -1
  100. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +2 -0
  101. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -1
  102. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -1
  103. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +28 -15
  104. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -1
  105. package/dist/services/datasource-introspection.service.d.ts +63 -2
  106. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  107. package/dist/services/datasource-introspection.service.js +665 -188
  108. package/dist/services/datasource-introspection.service.js.map +1 -1
  109. package/dist/services/import-transaction.service.d.ts.map +1 -1
  110. package/dist/services/import-transaction.service.js +3 -0
  111. package/dist/services/import-transaction.service.js.map +1 -1
  112. package/dist/services/locale.service.d.ts +14 -0
  113. package/dist/services/locale.service.d.ts.map +1 -1
  114. package/dist/services/locale.service.js +83 -0
  115. package/dist/services/locale.service.js.map +1 -1
  116. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +10 -10
  117. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  118. package/dist/services/menu-item-metadata.service.d.ts.map +1 -1
  119. package/dist/services/menu-item-metadata.service.js +1 -0
  120. package/dist/services/menu-item-metadata.service.js.map +1 -1
  121. package/dist/services/model-metadata.service.d.ts +3 -0
  122. package/dist/services/model-metadata.service.d.ts.map +1 -1
  123. package/dist/services/model-metadata.service.js +78 -0
  124. package/dist/services/model-metadata.service.js.map +1 -1
  125. package/dist/services/module-metadata.service.d.ts.map +1 -1
  126. package/dist/services/module-metadata.service.js +28 -0
  127. package/dist/services/module-metadata.service.js.map +1 -1
  128. package/dist/services/removed-field-migration.service.d.ts +30 -0
  129. package/dist/services/removed-field-migration.service.d.ts.map +1 -0
  130. package/dist/services/removed-field-migration.service.js +319 -0
  131. package/dist/services/removed-field-migration.service.js.map +1 -0
  132. package/dist/services/role-metadata.service.d.ts +1 -0
  133. package/dist/services/role-metadata.service.d.ts.map +1 -1
  134. package/dist/services/role-metadata.service.js +23 -2
  135. package/dist/services/role-metadata.service.js.map +1 -1
  136. package/dist/services/settings/default-settings-provider.service.d.ts +244 -0
  137. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  138. package/dist/services/settings/default-settings-provider.service.js +120 -0
  139. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  140. package/dist/services/solid-ts-morph.service.d.ts +6 -0
  141. package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
  142. package/dist/services/solid-ts-morph.service.js +43 -0
  143. package/dist/services/solid-ts-morph.service.js.map +1 -1
  144. package/dist/services/view-metadata.service.d.ts.map +1 -1
  145. package/dist/services/view-metadata.service.js +4 -7
  146. package/dist/services/view-metadata.service.js.map +1 -1
  147. package/dist/solid-core.module.d.ts.map +1 -1
  148. package/dist/solid-core.module.js +4 -0
  149. package/dist/solid-core.module.js.map +1 -1
  150. package/dist/subscribers/audit.subscriber.d.ts +5 -1
  151. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  152. package/dist/subscribers/audit.subscriber.js +76 -12
  153. package/dist/subscribers/audit.subscriber.js.map +1 -1
  154. package/dist/subscribers/computed-entity-field.subscriber.d.ts +9 -1
  155. package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
  156. package/dist/subscribers/computed-entity-field.subscriber.js +45 -11
  157. package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
  158. package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
  159. package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
  160. package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
  161. package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
  162. package/dist/subscribers/scheduled-job.subscriber.d.ts.map +1 -1
  163. package/dist/subscribers/scheduled-job.subscriber.js +3 -2
  164. package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
  165. package/dist/subscribers/security-rule.subscriber.d.ts.map +1 -1
  166. package/dist/subscribers/security-rule.subscriber.js +5 -3
  167. package/dist/subscribers/security-rule.subscriber.js.map +1 -1
  168. package/package.json +1 -1
  169. package/src/commands/migrate-removed-fields.command.ts +81 -0
  170. package/src/controllers/datasource-introspection.controller.ts +27 -1
  171. package/src/controllers/service.controller.ts +3 -2
  172. package/src/dtos/datasource-introspection-mapping.dto.ts +15 -0
  173. package/src/entities/{legacy-common-with-id.entity.ts → legacy-common-with-generated-id.entity.ts} +1 -1
  174. package/src/helpers/command.service.ts +10 -2
  175. package/src/helpers/environment.helper.ts +11 -0
  176. package/src/helpers/field-crud-managers/IntFieldCrudManager.ts +4 -1
  177. package/src/helpers/model-metadata-helper.service.ts +8 -3
  178. package/src/helpers/module-metadata-helper.service.ts +1 -1
  179. package/src/helpers/nodemon-heartbeat.ts +59 -0
  180. package/src/helpers/solid-registry.ts +7 -1
  181. package/src/helpers/typeorm-db-helper.ts +15 -0
  182. package/src/index.ts +3 -2
  183. package/src/interfaces/datasource-introspection-provider.interface.ts +1 -0
  184. package/src/interfaces.ts +1 -0
  185. package/src/repository/scheduled-job.repository.ts +10 -4
  186. package/src/repository/security-rule.repository.ts +10 -4
  187. package/src/seeders/module-metadata-seeder.service.ts +192 -2
  188. package/src/seeders/seed-data/solid-core-metadata.json +128 -34
  189. package/src/services/api-key.service.ts +36 -36
  190. package/src/services/authentication.service.ts +46 -12
  191. package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +11 -5
  192. package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +15 -1
  193. package/src/services/crud-helper.service.ts +25 -12
  194. package/src/services/crud.service.ts +36 -11
  195. package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +12 -0
  196. package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +2 -0
  197. package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +28 -15
  198. package/src/services/datasource-introspection.service.ts +827 -164
  199. package/src/services/import-transaction.service.ts +5 -0
  200. package/src/services/locale.service.ts +111 -3
  201. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +2 -2
  202. package/src/services/menu-item-metadata.service.ts +2 -1
  203. package/src/services/model-metadata.service.ts +109 -1
  204. package/src/services/module-metadata.service.ts +36 -0
  205. package/src/services/removed-field-migration.service.ts +334 -0
  206. package/src/services/role-metadata.service.ts +26 -2
  207. package/src/services/settings/default-settings-provider.service.ts +120 -0
  208. package/src/services/solid-ts-morph.service.ts +61 -1
  209. package/src/services/view-metadata.service.ts +4 -8
  210. package/src/solid-core.module.ts +4 -0
  211. package/src/subscribers/audit.subscriber.ts +101 -18
  212. package/src/subscribers/computed-entity-field.subscriber.ts +81 -28
  213. package/src/subscribers/created-by-updated-by.subscriber.ts +3 -0
  214. package/src/subscribers/field-metadata.subscriber.ts +3 -0
  215. package/src/subscribers/scheduled-job.subscriber.ts +3 -2
  216. package/src/subscribers/security-rule.subscriber.ts +8 -3
  217. package/dist/entities/legacy-common-with-id.entity.d.ts.map +0 -1
  218. package/dist/entities/legacy-common-with-id.entity.js.map +0 -1
  219. package/dist/entities/legacy-common.entity.d.ts.map +0 -1
  220. package/dist/entities/legacy-common.entity.js.map +0 -1
  221. /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";
@@ -39,11 +43,16 @@ type TableInventoryRecord = {
39
43
  suggestedUserKeyField: string;
40
44
  columnCount: number;
41
45
  physicalColumnCount: number;
46
+ hasPrimaryKey: boolean;
47
+ primaryKeyColumnCount: number;
48
+ primaryKeyColumnNames: string[];
49
+ mappingBlockedReason: string | null;
42
50
  };
43
51
 
44
52
  type TableDetailColumnRecord = {
45
53
  columnName: string;
46
54
  fieldName: string;
55
+ displayName: string;
47
56
  dataType: string;
48
57
  solidFieldType: string;
49
58
  ormType: string;
@@ -54,6 +63,7 @@ type TableDetailColumnRecord = {
54
63
  numericScale: number | null;
55
64
  ordinalPosition: number | null;
56
65
  isAutoGenerated: boolean;
66
+ isPrimaryKey: boolean;
57
67
  handledBySuperclass: boolean;
58
68
  superclassFieldName: string | null;
59
69
  };
@@ -81,6 +91,13 @@ type MappingPreviewRecord = {
81
91
  note: string;
82
92
  missingSystemColumns: string[];
83
93
  };
94
+ datasourceFile: {
95
+ filePath: string;
96
+ created: boolean;
97
+ updated: boolean;
98
+ entityRegistered: boolean;
99
+ migrationGlobRegistered: boolean;
100
+ };
84
101
  summary: {
85
102
  includedColumnNames: string[];
86
103
  removedColumnNames: string[];
@@ -119,6 +136,105 @@ type SystemColumnDefinition = {
119
136
  isUnique?: boolean;
120
137
  };
121
138
 
139
+ type DatasourceTypeFamily =
140
+ | "boolean"
141
+ | "bigint"
142
+ | "int"
143
+ | "numeric"
144
+ | "decimal"
145
+ | "float"
146
+ | "date"
147
+ | "time"
148
+ | "datetime"
149
+ | "datetimeWithTimezone"
150
+ | "json"
151
+ | "jsonb"
152
+ | "uuid"
153
+ | "longText"
154
+ | "mediumText"
155
+ | "shortText"
156
+ | "unicodeShortText";
157
+
158
+ const DATASOURCE_TYPE_FAMILY_ALIASES: Record<DatasourceTypeFamily, string[]> = {
159
+ boolean: ["bit", "bool", "boolean"],
160
+ bigint: ["bigint"],
161
+ int: ["int", "integer", "smallint", "tinyint"],
162
+ numeric: ["numeric"],
163
+ decimal: ["decimal", "money", "smallmoney"],
164
+ float: ["float", "double", "double precision", "real", "float4", "float8"],
165
+ date: ["date"],
166
+ time: ["time", "time without time zone", "time with time zone"],
167
+ datetime: ["datetime", "datetime2", "smalldatetime", "timestamp", "timestamp without time zone"],
168
+ datetimeWithTimezone: ["timestamptz", "timestamp with time zone"],
169
+ json: ["json", "simple-json", "simplejson"],
170
+ jsonb: ["jsonb"],
171
+ uuid: ["uniqueidentifier", "uuid"],
172
+ longText: ["text", "ntext", "longtext", "clob"],
173
+ mediumText: ["mediumtext"],
174
+ shortText: ["varchar", "char", "citext"],
175
+ unicodeShortText: ["nvarchar", "nchar"],
176
+ };
177
+
178
+ const ORM_TYPE_BY_DATASOURCE_FAMILY: Record<string, Partial<Record<DatasourceTypeFamily, string>>> = {
179
+ mssql: {
180
+ boolean: "bit",
181
+ bigint: "bigint",
182
+ int: "int",
183
+ numeric: "numeric",
184
+ decimal: "decimal",
185
+ float: "decimal",
186
+ date: "date",
187
+ time: "time",
188
+ datetime: "datetime",
189
+ datetimeWithTimezone: "datetime",
190
+ json: "simple-json",
191
+ jsonb: "simple-json",
192
+ uuid: "uniqueidentifier",
193
+ longText: "text",
194
+ mediumText: "text",
195
+ shortText: "varchar",
196
+ unicodeShortText: "nvarchar",
197
+ },
198
+ mysql: {
199
+ boolean: "boolean",
200
+ bigint: "int",
201
+ int: "int",
202
+ numeric: "decimal",
203
+ decimal: "decimal",
204
+ float: "double",
205
+ date: "date",
206
+ time: "time",
207
+ datetime: "datetime",
208
+ datetimeWithTimezone: "datetime",
209
+ json: "json",
210
+ jsonb: "json",
211
+ uuid: "varchar",
212
+ longText: "longtext",
213
+ mediumText: "mediumtext",
214
+ shortText: "varchar",
215
+ unicodeShortText: "varchar",
216
+ },
217
+ postgres: {
218
+ boolean: "boolean",
219
+ bigint: "bigint",
220
+ int: "integer",
221
+ numeric: "decimal",
222
+ decimal: "decimal",
223
+ float: "decimal",
224
+ date: "date",
225
+ time: "time",
226
+ datetime: "timestamp",
227
+ datetimeWithTimezone: "timestamptz",
228
+ json: "json",
229
+ jsonb: "jsonb",
230
+ uuid: "uuid",
231
+ longText: "text",
232
+ mediumText: "text",
233
+ shortText: "varchar",
234
+ unicodeShortText: "varchar",
235
+ },
236
+ };
237
+
122
238
  @Injectable()
123
239
  export class DatasourceIntrospectionService {
124
240
  private readonly supportedDatasourceTypes = new Set(["mssql", "mysql", "postgres"]);
@@ -132,6 +248,8 @@ export class DatasourceIntrospectionService {
132
248
  private readonly modelMetadataHelperService: ModelMetadataHelperService,
133
249
  private readonly fieldMetadataService: FieldMetadataService,
134
250
  private readonly modelMetadataService: ModelMetadataService,
251
+ private readonly moduleMetadataService: ModuleMetadataService,
252
+ private readonly commandService: CommandService,
135
253
  private readonly mssqlProvider: MssqlDatasourceIntrospectionProviderService,
136
254
  private readonly mysqlProvider: MysqlDatasourceIntrospectionProviderService,
137
255
  private readonly postgresProvider: PostgresDatasourceIntrospectionProviderService,
@@ -266,11 +384,6 @@ export class DatasourceIntrospectionService {
266
384
  ? await this.modelMetadataService.update(mappingDto.modelId, plan.modelPayload as UpdateModelMetaDataDto)
267
385
  : await this.modelMetadataService.create(plan.modelPayload as CreateModelMetadataDto);
268
386
 
269
- if (plan.migration.willGenerate && plan.migration.content && plan.migration.filePath) {
270
- await fs.mkdir(path.dirname(plan.migration.filePath), { recursive: true });
271
- await fs.writeFile(plan.migration.filePath, plan.migration.content, "utf8");
272
- }
273
-
274
387
  const persistedModelId = mappingDto.modelId ?? (persistedModel as ModelMetadata | undefined)?.id ?? null;
275
388
 
276
389
  return {
@@ -278,15 +391,106 @@ export class DatasourceIntrospectionService {
278
391
  modelId: persistedModelId,
279
392
  operation: plan.operation,
280
393
  metadataJson: plan.metadataJson,
394
+ summary: plan.summary,
395
+ },
396
+ };
397
+ }
398
+
399
+ async createMigrationArtifacts(moduleId: number, mappingDto: DatasourceIntrospectionMappingDto) {
400
+ const plan = await this.buildMappingPreview(moduleId, mappingDto);
401
+ const module = await this.loadModule(moduleId);
402
+ const datasource = this.requireDatasource(mappingDto.dataSource);
403
+
404
+ if (!mappingDto.modelId) {
405
+ throw new BadRequestException("Persist the model mapping first so the generated entity can be registered before creating migration artifacts.");
406
+ }
407
+
408
+ const migrationWritten = await this.writeMigrationFile(plan.migration);
409
+
410
+ const datasourceFile = await this.syncTypeormDatasourceFile(
411
+ module.name,
412
+ datasource,
413
+ plan.modelPayload.singularName,
414
+ );
415
+
416
+ return {
417
+ data: {
418
+ modelId: mappingDto.modelId,
419
+ operation: plan.operation,
281
420
  migration: {
282
421
  ...plan.migration,
283
- written: Boolean(plan.migration.willGenerate && plan.migration.filePath),
422
+ written: migrationWritten,
284
423
  },
424
+ datasourceFile,
285
425
  summary: plan.summary,
286
426
  },
287
427
  };
288
428
  }
289
429
 
430
+ async generateCode(moduleId: number) {
431
+ await this.loadModule(moduleId);
432
+ const output = await this.moduleMetadataService.generateCodeViaCtl(moduleId);
433
+
434
+ return {
435
+ data: {
436
+ moduleId,
437
+ output,
438
+ message: "Module code generation completed.",
439
+ },
440
+ };
441
+ }
442
+
443
+ async runMigration(moduleId: number, datasourceName: string) {
444
+ const module = await this.loadModule(moduleId);
445
+ const datasource = this.requireDatasource(datasourceName);
446
+ const cwd = path.join(process.cwd(), "..");
447
+ const outputs: string[] = [];
448
+
449
+ const migrationOutput = await this.commandService.executeCommandWithArgs({
450
+ command: "npx",
451
+ args: ["-y", "@solidxai/solidctl@latest", "migration", "-d", datasource.name, "run"],
452
+ cwd,
453
+ });
454
+ outputs.push([
455
+ `npx @solidxai/solidctl@latest migration -d ${datasource.name} run`,
456
+ migrationOutput,
457
+ ].filter(Boolean).join("\n"));
458
+
459
+ const buildOutput = await this.commandService.executeCommandWithArgs({
460
+ command: "npx",
461
+ args: ["-y", "@solidxai/solidctl@latest", "build"],
462
+ cwd,
463
+ });
464
+ outputs.push([
465
+ "npx @solidxai/solidctl@latest build",
466
+ buildOutput,
467
+ ].filter(Boolean).join("\n"));
468
+
469
+ const seedOutput = await this.commandService.executeCommandWithArgs({
470
+ command: "npx",
471
+ args: ["-y", "@solidxai/solidctl@latest", "seed", "--modules-to-seed", module.name],
472
+ cwd,
473
+ });
474
+ outputs.push([
475
+ `npx @solidxai/solidctl@latest seed --modules-to-seed ${module.name}`,
476
+ seedOutput,
477
+ ].filter(Boolean).join("\n"));
478
+
479
+ return {
480
+ data: {
481
+ moduleId,
482
+ datasource: datasource.name,
483
+ output: outputs.join("\n\n"),
484
+ command: [
485
+ `npx @solidxai/solidctl@latest migration -d ${datasource.name} run`,
486
+ "npx @solidxai/solidctl@latest build",
487
+ `npx @solidxai/solidctl@latest seed --modules-to-seed ${module.name}`,
488
+ ].join("\n"),
489
+ message: `Migration, build, and seed completed successfully for datasource "${datasource.name}".`,
490
+ },
491
+ };
492
+ }
493
+
290
494
  private async buildMappingPreview(moduleId: number, mappingDto: DatasourceIntrospectionMappingDto): Promise<MappingPreviewRecord> {
291
495
  const module = await this.loadModule(moduleId);
292
496
  const datasource = this.requireDatasource(mappingDto.dataSource);
@@ -301,6 +505,8 @@ export class DatasourceIntrospectionService {
301
505
  if (!tableRecord) {
302
506
  throw new NotFoundException(`Table "${mappingDto.tableName}" was not found on datasource "${datasource.name}".`);
303
507
  }
508
+ const tableColumns = inventory.columnsByKey.get(tableKey) ?? [];
509
+ this.validateTablePrimaryKeyForMapping(tableRecord, tableColumns, mappingDto);
304
510
 
305
511
  const existingModel = mappingDto.mapped && mappingDto.modelId
306
512
  ? await this.modelMetadataRepository.findOne({
@@ -318,14 +524,14 @@ export class DatasourceIntrospectionService {
318
524
  throw new NotFoundException(`Mapped model "${mappingDto.modelId}" was not found.`);
319
525
  }
320
526
 
321
- const reviewedModel = this.normalizeReviewedMetadataModel(mappingDto.reviewedModel, mappingDto);
527
+ const reviewedModel = this.normalizeReviewedMetadataModel(mappingDto.reviewedModel, mappingDto, tableColumns);
322
528
  const modelPayload = reviewedModel
323
529
  ? this.buildModelPayloadFromReviewedModel(moduleId, mappingDto, existingModel, reviewedModel)
324
530
  : this.buildModelPayload(moduleId, mappingDto, existingModel);
325
531
  const metadataJson = reviewedModel
326
532
  ? await this.buildMetadataJsonPreviewFromReviewedModel(module.name, reviewedModel)
327
533
  : await this.buildMetadataJsonPreview(module.name, mappingDto, existingModel);
328
- const migration = await this.buildMigrationPreview(module.name, datasource, mappingDto, inventory.columnsByKey.get(tableKey) ?? []);
534
+ const migration = await this.buildMigrationPreview(module.name, datasource, mappingDto, tableColumns);
329
535
  const includedColumnNames = mappingDto.columns
330
536
  .filter((column) => !column.handledBySuperclass && column.include)
331
537
  .map((column) => column.columnName);
@@ -338,6 +544,13 @@ export class DatasourceIntrospectionService {
338
544
  mappedModelId: mappingDto.modelId ?? null,
339
545
  metadataJson,
340
546
  migration,
547
+ datasourceFile: {
548
+ filePath: path.resolve(process.cwd(), "src", `typeorm-${datasource.name}-datasource.ts`),
549
+ created: false,
550
+ updated: false,
551
+ entityRegistered: false,
552
+ migrationGlobRegistered: false,
553
+ },
341
554
  summary: {
342
555
  includedColumnNames,
343
556
  removedColumnNames,
@@ -399,6 +612,11 @@ export class DatasourceIntrospectionService {
399
612
  existingModel: ModelMetadata | null,
400
613
  reviewedModel: ReviewedMetadataModel,
401
614
  ): CreateModelMetadataDto | UpdateModelMetaDataDto {
615
+ const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
616
+ reviewedModel.fields,
617
+ reviewedModel.userKeyFieldUserKey,
618
+ mappingDto.userKeyField,
619
+ );
402
620
  const systemFieldPayloads = existingModel
403
621
  ? existingModel.fields
404
622
  .filter((field) => field.isSystem)
@@ -410,7 +628,7 @@ export class DatasourceIntrospectionService {
410
628
  return {
411
629
  ...field,
412
630
  ...(matchedExistingField?.id ? { id: matchedExistingField.id } : {}),
413
- isUserKey: field.name === reviewedModel.userKeyFieldUserKey,
631
+ isUserKey: field.name === resolvedUserKeyFieldName,
414
632
  isSystem: field.isSystem ?? false,
415
633
  };
416
634
  });
@@ -444,6 +662,11 @@ export class DatasourceIntrospectionService {
444
662
  ) {
445
663
  const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
446
664
  const previewFields = await this.buildMetadataPreviewFields(mappingDto, existingModel);
665
+ const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
666
+ previewFields,
667
+ mappingDto.userKeyField,
668
+ mappingDto.userKeyField,
669
+ ) ?? mappingDto.userKeyField;
447
670
 
448
671
  return {
449
672
  filePath,
@@ -455,7 +678,7 @@ export class DatasourceIntrospectionService {
455
678
  dataSource: mappingDto.dataSource,
456
679
  dataSourceType: mappingDto.dataSourceType,
457
680
  tableName: mappingDto.tableName,
458
- userKeyFieldUserKey: mappingDto.userKeyField,
681
+ userKeyFieldUserKey: resolvedUserKeyFieldName,
459
682
  isChild: false,
460
683
  legacyTableType: mappingDto.legacyTableType,
461
684
  parentModelUserKey: "",
@@ -523,10 +746,6 @@ export class DatasourceIntrospectionService {
523
746
 
524
747
  const serialized = Array.from(finalFields.values())
525
748
  .filter((field) => !field.isSystem)
526
- .map((field) => ({
527
- ...field,
528
- isUserKey: field.name === mappingDto.userKeyField,
529
- }))
530
749
  .sort((left, right) => {
531
750
  const leftOrder = typeof left.id === "number" ? left.id : Number.MAX_SAFE_INTEGER;
532
751
  const rightOrder = typeof right.id === "number" ? right.id : Number.MAX_SAFE_INTEGER;
@@ -542,6 +761,16 @@ export class DatasourceIntrospectionService {
542
761
  previewFields.push(await this.fieldMetadataService.createFieldConfig(field as any));
543
762
  }
544
763
 
764
+ const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
765
+ previewFields,
766
+ mappingDto.userKeyField,
767
+ mappingDto.userKeyField,
768
+ );
769
+
770
+ for (const field of previewFields) {
771
+ field.isUserKey = field.name === resolvedUserKeyFieldName;
772
+ }
773
+
545
774
  return previewFields;
546
775
  }
547
776
 
@@ -608,9 +837,18 @@ export class DatasourceIntrospectionService {
608
837
  return rest;
609
838
  }
610
839
 
840
+ private sanitizeConfiguredFieldPayload(fieldConfig: Record<string, any> | undefined) {
841
+ if (!fieldConfig || typeof fieldConfig !== "object" || Array.isArray(fieldConfig)) {
842
+ return {};
843
+ }
844
+
845
+ return this.sanitizeExistingFieldPayload(fieldConfig);
846
+ }
847
+
611
848
  private normalizeReviewedMetadataModel(
612
849
  reviewedModel: Record<string, any> | undefined,
613
850
  mappingDto: DatasourceIntrospectionMappingDto,
851
+ tableColumns: DatasourceIntrospectionColumn[],
614
852
  ): ReviewedMetadataModel | null {
615
853
  if (!reviewedModel) {
616
854
  return null;
@@ -670,11 +908,24 @@ export class DatasourceIntrospectionService {
670
908
  fieldNames.add(loweredName);
671
909
  }
672
910
 
673
- const userKeyFieldUserKey = readString("userKeyFieldUserKey");
674
- if (!normalizedFields.some((field) => field.name === userKeyFieldUserKey && !field.isMarkedForRemoval)) {
911
+ const requestedUserKeyFieldUserKey = readString("userKeyFieldUserKey");
912
+ const resolvedUserKeyFieldName = this.resolveReviewedUserKeyFieldName(
913
+ normalizedFields,
914
+ requestedUserKeyFieldUserKey,
915
+ mappingDto.userKeyField,
916
+ );
917
+ const inheritedGeneratedIdUserKey = this.isGeneratedIdInheritedUserKey(
918
+ mappingDto.legacyTableType,
919
+ requestedUserKeyFieldUserKey,
920
+ );
921
+ if (!resolvedUserKeyFieldName && !inheritedGeneratedIdUserKey) {
675
922
  throw new BadRequestException("Reviewed metadata JSON must keep the selected userKeyFieldUserKey in the fields array.");
676
923
  }
677
924
 
925
+ this.validateReviewedPrimaryKeyFields(normalizedFields, tableColumns);
926
+
927
+ const userKeyFieldUserKey = resolvedUserKeyFieldName ?? requestedUserKeyFieldUserKey;
928
+
678
929
  return {
679
930
  singularName: readString("singularName"),
680
931
  pluralName: readString("pluralName"),
@@ -693,11 +944,54 @@ export class DatasourceIntrospectionService {
693
944
  internationalisation: readBoolean("internationalisation"),
694
945
  fields: normalizedFields.map((field) => ({
695
946
  ...field,
696
- isUserKey: field.name === userKeyFieldUserKey,
947
+ isUserKey: field.name === resolvedUserKeyFieldName,
697
948
  })),
698
949
  };
699
950
  }
700
951
 
952
+ private resolveReviewedUserKeyFieldName(
953
+ reviewedFields: Array<Record<string, any>>,
954
+ requestedUserKeyFieldName: string,
955
+ fallbackUserKeyFieldName?: string,
956
+ ) {
957
+ const activeFields = reviewedFields.filter((field) => !field.isMarkedForRemoval);
958
+ const normalizedRequested = `${requestedUserKeyFieldName ?? ""}`.trim().toLowerCase();
959
+ const normalizedFallback = `${fallbackUserKeyFieldName ?? ""}`.trim().toLowerCase();
960
+
961
+ const byExactRequested = activeFields.find((field) => field.name?.toLowerCase?.() === normalizedRequested);
962
+ if (byExactRequested) {
963
+ return byExactRequested.name;
964
+ }
965
+
966
+ const byExistingUserKeyFlag = activeFields.find((field) => field.isUserKey);
967
+ if (byExistingUserKeyFlag) {
968
+ return byExistingUserKeyFlag.name;
969
+ }
970
+
971
+ const byPrimaryKeyFallback = activeFields.find((field) => {
972
+ if (!field.isPrimaryKey) {
973
+ return false;
974
+ }
975
+
976
+ const normalizedName = `${field.name ?? ""}`.trim().toLowerCase();
977
+ return normalizedName === normalizedFallback || normalizedName === "id" || normalizedName === "legacyid";
978
+ });
979
+ if (byPrimaryKeyFallback) {
980
+ return byPrimaryKeyFallback.name;
981
+ }
982
+
983
+ const byFallbackFieldName = activeFields.find((field) => field.name?.toLowerCase?.() === normalizedFallback);
984
+ return byFallbackFieldName?.name ?? null;
985
+ }
986
+
987
+ private isGeneratedIdInheritedUserKey(
988
+ legacyTableType: LegacyTableType,
989
+ requestedUserKeyFieldName: string | null | undefined,
990
+ ) {
991
+ return legacyTableType === LegacyTableType.GENERATED_ID
992
+ && `${requestedUserKeyFieldName ?? ""}`.trim().toLowerCase() === "id";
993
+ }
994
+
701
995
  private normalizeReviewedField(field: any, index: number) {
702
996
  if (!field || typeof field !== "object" || Array.isArray(field)) {
703
997
  throw new BadRequestException(`Reviewed metadata JSON field at index ${index} is not a valid object.`);
@@ -762,34 +1056,39 @@ export class DatasourceIntrospectionService {
762
1056
  existingField?: any,
763
1057
  ) {
764
1058
  const preserved = existingField ? this.sanitizeExistingFieldPayload(existingField) : {};
1059
+ const configured = this.sanitizeConfiguredFieldPayload(column.fieldConfig);
1060
+ const merged = {
1061
+ ...preserved,
1062
+ ...configured,
1063
+ };
765
1064
  const maxLength =
766
1065
  column.characterMaximumLength && ["shortText", "longText"].includes(column.solidFieldType)
767
1066
  ? column.characterMaximumLength
768
- : preserved.max;
1067
+ : merged.max;
769
1068
 
770
1069
  return {
771
- ...preserved,
1070
+ ...merged,
772
1071
  ...(column.fieldId ? { id: column.fieldId } : {}),
773
1072
  name: column.fieldName,
774
- displayName: preserved.displayName ?? startCase(column.fieldName),
775
- description: preserved.description ?? "",
1073
+ displayName: column.displayName?.trim() || merged.displayName || startCase(column.fieldName),
1074
+ description: merged.description ?? "",
776
1075
  type: column.solidFieldType,
777
1076
  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,
1077
+ length: merged.length ?? column.characterMaximumLength ?? undefined,
1078
+ defaultValue: merged.defaultValue ?? column.defaultValue ?? undefined,
1079
+ required: merged.required ?? (!column.nullable || column.isPrimaryKey),
1080
+ unique: merged.unique ?? false,
1081
+ encrypt: merged.encrypt ?? false,
1082
+ index: merged.index ?? false,
784
1083
  max: maxLength ?? undefined,
785
- min: preserved.min ?? undefined,
786
- private: preserved.private ?? false,
787
- isSystem: preserved.isSystem ?? false,
1084
+ min: merged.min ?? undefined,
1085
+ private: merged.private ?? false,
1086
+ isSystem: merged.isSystem ?? false,
788
1087
  isMarkedForRemoval: false,
789
1088
  columnName: column.columnName,
790
1089
  isUserKey: mappingDto.userKeyField === column.fieldName,
791
- enableAuditTracking: preserved.enableAuditTracking ?? mappingDto.enableAuditTracking,
792
- isMultiSelect: preserved.isMultiSelect ?? false,
1090
+ enableAuditTracking: merged.enableAuditTracking ?? mappingDto.enableAuditTracking,
1091
+ isMultiSelect: merged.isMultiSelect ?? false,
793
1092
  isPrimaryKey: column.isPrimaryKey,
794
1093
  };
795
1094
  }
@@ -891,11 +1190,8 @@ export class DatasourceIntrospectionService {
891
1190
 
892
1191
  const upColumnStatements = missingSystemColumns.map((definition) => {
893
1192
  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");
1193
+ return ` if (!(await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)})))\n`
1194
+ + ` await queryRunner.addColumn(tableName, new TableColumn(${columnLiteral}));`;
899
1195
  }).join("\n\n");
900
1196
 
901
1197
  const postUpStatements: string[] = [];
@@ -904,12 +1200,11 @@ export class DatasourceIntrospectionService {
904
1200
  [
905
1201
  " {",
906
1202
  " 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
- " }",
1203
+ ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(deletedAtIndexName)}))`,
1204
+ ` await queryRunner.createIndex(tableName, new TableIndex(${this.renderTableIndexLiteral({
1205
+ name: deletedAtIndexName,
1206
+ columnNames: ["ss_deleted_at"],
1207
+ })}));`,
913
1208
  " }",
914
1209
  ].join("\n"),
915
1210
  );
@@ -919,13 +1214,12 @@ export class DatasourceIntrospectionService {
919
1214
  [
920
1215
  " {",
921
1216
  " 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
- " }",
1217
+ ` if (table && !table.indices.some((index) => index.name === ${JSON.stringify(generatedIdUniqueName)}))`,
1218
+ ` await queryRunner.createIndex(tableName, new TableIndex(${this.renderTableIndexLiteral({
1219
+ name: generatedIdUniqueName,
1220
+ columnNames: ["ss_id"],
1221
+ isUnique: true,
1222
+ })}));`,
929
1223
  " }",
930
1224
  ].join("\n"),
931
1225
  );
@@ -938,9 +1232,8 @@ export class DatasourceIntrospectionService {
938
1232
  " {",
939
1233
  " const table = await queryRunner.getTable(tableName);",
940
1234
  ` const index = table?.indices.find((entry) => entry.name === ${JSON.stringify(generatedIdUniqueName)});`,
941
- " if (index) {",
1235
+ " if (index)",
942
1236
  " await queryRunner.dropIndex(tableName, index);",
943
- " }",
944
1237
  " }",
945
1238
  ].join("\n"),
946
1239
  );
@@ -951,9 +1244,8 @@ export class DatasourceIntrospectionService {
951
1244
  " {",
952
1245
  " const table = await queryRunner.getTable(tableName);",
953
1246
  ` const index = table?.indices.find((entry) => entry.name === ${JSON.stringify(deletedAtIndexName)});`,
954
- " if (index) {",
1247
+ " if (index)",
955
1248
  " await queryRunner.dropIndex(tableName, index);",
956
- " }",
957
1249
  " }",
958
1250
  ].join("\n"),
959
1251
  );
@@ -962,11 +1254,8 @@ export class DatasourceIntrospectionService {
962
1254
  ...missingSystemColumns
963
1255
  .slice()
964
1256
  .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")),
1257
+ .map((definition) => ` if (await queryRunner.hasColumn(tableName, ${JSON.stringify(definition.columnName)}))\n`
1258
+ + ` await queryRunner.dropColumn(tableName, ${JSON.stringify(definition.columnName)});`),
970
1259
  );
971
1260
 
972
1261
  return [
@@ -992,29 +1281,41 @@ export class DatasourceIntrospectionService {
992
1281
 
993
1282
  private renderTableColumnLiteral(definition: SystemColumnDefinition) {
994
1283
  const properties: string[] = [
995
- `{\n name: ${JSON.stringify(definition.columnName)},`,
996
- ` type: ${JSON.stringify(definition.type)},`,
1284
+ `name: ${JSON.stringify(definition.columnName)}`,
1285
+ `type: ${JSON.stringify(definition.type)}`,
997
1286
  ];
998
1287
 
999
1288
  if (definition.length) {
1000
- properties.push(` length: ${JSON.stringify(definition.length)},`);
1289
+ properties.push(`length: ${JSON.stringify(definition.length)}`);
1001
1290
  }
1002
1291
  if (typeof definition.isNullable === "boolean") {
1003
- properties.push(` isNullable: ${definition.isNullable},`);
1292
+ properties.push(`isNullable: ${definition.isNullable}`);
1004
1293
  }
1005
1294
  if (definition.defaultExpression) {
1006
- properties.push(` default: ${JSON.stringify(definition.defaultExpression)},`);
1295
+ properties.push(`default: ${JSON.stringify(definition.defaultExpression)}`);
1007
1296
  }
1008
1297
  if (definition.isGenerated) {
1009
- properties.push(" isGenerated: true,");
1010
- properties.push(" generationStrategy: \"increment\",");
1298
+ properties.push("isGenerated: true");
1299
+ properties.push("generationStrategy: \"increment\"");
1011
1300
  }
1012
1301
  if (definition.isUnique) {
1013
- properties.push(" isUnique: true,");
1302
+ properties.push("isUnique: true");
1303
+ }
1304
+
1305
+ return `{ ${properties.join(", ")}, }`;
1306
+ }
1307
+
1308
+ private renderTableIndexLiteral(definition: { name: string; columnNames: string[]; isUnique?: boolean }) {
1309
+ const properties: string[] = [
1310
+ `name: ${JSON.stringify(definition.name)}`,
1311
+ `columnNames: ${JSON.stringify(definition.columnNames)}`,
1312
+ ];
1313
+
1314
+ if (definition.isUnique) {
1315
+ properties.push("isUnique: true");
1014
1316
  }
1015
1317
 
1016
- properties.push(" }");
1017
- return properties.join("\n");
1318
+ return `{ ${properties.join(", ")}, }`;
1018
1319
  }
1019
1320
 
1020
1321
  private buildMigrationObjectName(prefix: string, tableName: string, columnName: string) {
@@ -1050,13 +1351,15 @@ export class DatasourceIntrospectionService {
1050
1351
  private async buildTableInventory(datasource: DatasourceRecord) {
1051
1352
  const provider = this.getProvider(datasource.type);
1052
1353
  const runtimeDataSource = this.resolveDataSourceByName(datasource.name);
1053
- const [tables, columns, models] = await Promise.all([
1354
+ const [rawTables, rawColumns, models] = await Promise.all([
1054
1355
  provider.listTables(runtimeDataSource),
1055
1356
  provider.listColumns(runtimeDataSource),
1056
1357
  this.modelMetadataRepository.find({ relations: { module: true } as any }),
1057
1358
  ]);
1359
+ const tables = rawTables.filter((table) => !this.isSolidxInternalTableName(table.tableName));
1360
+ const columns = rawColumns.filter((column) => !this.isSolidxInternalTableName(column.tableName));
1058
1361
 
1059
- const modelMap = this.buildModelMap(models);
1362
+ const modelLookup = this.buildModelLookup(models);
1060
1363
  const columnsByKey = this.groupColumnsByTable(columns);
1061
1364
  const tableKeys = new Set<string>();
1062
1365
 
@@ -1080,8 +1383,10 @@ export class DatasourceIntrospectionService {
1080
1383
 
1081
1384
  const suggestedLegacyTableType = this.detectLegacyTableType(datasource.type, tableColumns);
1082
1385
  const suggestedIdColumn = this.getSuggestedIdColumn(datasource.type, tableColumns, suggestedLegacyTableType);
1083
- const mappedModel = modelMap.get(this.toModelMapKey(datasource.name, tableName)) ?? null;
1386
+ const mappedModel = this.resolveMappedModel(modelLookup, datasource.name, tableName);
1084
1387
  const physicalColumns = tableColumns.filter((column) => !this.isHandledBySuperclass(column.columnName, suggestedLegacyTableType));
1388
+ const primaryKeyColumns = tableColumns.filter((column) => column.isPrimaryKey);
1389
+ const hasPrimaryKey = primaryKeyColumns.length > 0;
1085
1390
 
1086
1391
  return {
1087
1392
  schema,
@@ -1099,6 +1404,12 @@ export class DatasourceIntrospectionService {
1099
1404
  suggestedUserKeyField: this.buildUserKeyCandidates(tableColumns)[0]?.name ?? "id",
1100
1405
  columnCount: tableColumns.length,
1101
1406
  physicalColumnCount: physicalColumns.length,
1407
+ hasPrimaryKey,
1408
+ primaryKeyColumnCount: primaryKeyColumns.length,
1409
+ primaryKeyColumnNames: primaryKeyColumns.map((column) => column.columnName),
1410
+ mappingBlockedReason: hasPrimaryKey
1411
+ ? null
1412
+ : this.buildMissingPrimaryKeyMessage(tableName),
1102
1413
  } satisfies TableInventoryRecord;
1103
1414
  })
1104
1415
  .filter((record): record is TableInventoryRecord => Boolean(record))
@@ -1119,19 +1430,52 @@ export class DatasourceIntrospectionService {
1119
1430
  };
1120
1431
  }
1121
1432
 
1122
- private buildModelMap(models: ModelMetadata[]) {
1123
- const map = new Map<string, ModelMetadata>();
1433
+ private buildModelLookup(models: ModelMetadata[]) {
1434
+ const byDatasourceAndTable = new Map<string, ModelMetadata>();
1435
+ const byTableName = new Map<string, ModelMetadata[]>();
1124
1436
 
1125
1437
  for (const model of models) {
1126
1438
  if (!model.tableName) {
1127
1439
  continue;
1128
1440
  }
1129
1441
 
1130
- map.set(this.toModelMapKey(model.dataSource, model.tableName), model);
1131
- map.set(this.toModelMapKey("*", model.tableName), model);
1442
+ byDatasourceAndTable.set(this.toModelMapKey(model.dataSource, model.tableName), model);
1443
+
1444
+ const normalizedTableName = model.tableName.trim().toLowerCase();
1445
+ const bucket = byTableName.get(normalizedTableName);
1446
+ if (bucket) {
1447
+ bucket.push(model);
1448
+ continue;
1449
+ }
1450
+
1451
+ byTableName.set(normalizedTableName, [model]);
1132
1452
  }
1133
1453
 
1134
- return map;
1454
+ return {
1455
+ byDatasourceAndTable,
1456
+ byTableName,
1457
+ };
1458
+ }
1459
+
1460
+ private resolveMappedModel(
1461
+ lookup: {
1462
+ byDatasourceAndTable: Map<string, ModelMetadata>;
1463
+ byTableName: Map<string, ModelMetadata[]>;
1464
+ },
1465
+ datasourceName: string,
1466
+ tableName: string,
1467
+ ) {
1468
+ const exactMatch = lookup.byDatasourceAndTable.get(this.toModelMapKey(datasourceName, tableName));
1469
+ if (exactMatch) {
1470
+ return exactMatch;
1471
+ }
1472
+
1473
+ const fallbackMatches = lookup.byTableName.get(tableName.trim().toLowerCase()) ?? [];
1474
+ if (fallbackMatches.length === 1) {
1475
+ return fallbackMatches[0];
1476
+ }
1477
+
1478
+ return fallbackMatches[0] ?? null;
1135
1479
  }
1136
1480
 
1137
1481
  private groupColumnsByTable(columns: DatasourceIntrospectionColumn[]) {
@@ -1151,6 +1495,10 @@ export class DatasourceIntrospectionService {
1151
1495
  return grouped;
1152
1496
  }
1153
1497
 
1498
+ private isSolidxInternalTableName(tableName: string | null | undefined) {
1499
+ return typeof tableName === "string" && tableName.toLowerCase().startsWith("ss_");
1500
+ }
1501
+
1154
1502
  private detectLegacyTableType(datasourceType: string, columns: DatasourceIntrospectionColumn[]): LegacyTableType {
1155
1503
  const normalizedDatasourceType = datasourceType.toLowerCase();
1156
1504
  const hasExistingGeneratedId = columns.some((column) => column.isAutoGenerated);
@@ -1239,13 +1587,17 @@ export class DatasourceIntrospectionService {
1239
1587
  datasourceType: string,
1240
1588
  ): TableDetailColumnRecord {
1241
1589
  const superclassFieldName = this.getSuperclassFieldName(column.columnName, legacyTableType);
1590
+ const normalizedColumnName = column.columnName.toLowerCase();
1242
1591
  const fieldName = legacyTableType === LegacyTableType.EXISTING_ID && column.isAutoGenerated
1243
1592
  ? "id"
1244
- : this.toFieldName(column.columnName);
1593
+ : legacyTableType === LegacyTableType.GENERATED_ID && normalizedColumnName === "id"
1594
+ ? "legacyId"
1595
+ : this.toFieldName(column.columnName);
1245
1596
 
1246
1597
  return {
1247
1598
  columnName: column.columnName,
1248
1599
  fieldName,
1600
+ displayName: startCase(fieldName),
1249
1601
  dataType: column.dataType,
1250
1602
  solidFieldType: this.toSolidFieldType(column),
1251
1603
  ormType: this.toOrmFieldType(column, datasourceType),
@@ -1256,11 +1608,72 @@ export class DatasourceIntrospectionService {
1256
1608
  numericScale: column.numericScale,
1257
1609
  ordinalPosition: column.ordinalPosition,
1258
1610
  isAutoGenerated: column.isAutoGenerated,
1611
+ isPrimaryKey: column.isPrimaryKey,
1259
1612
  handledBySuperclass: Boolean(superclassFieldName),
1260
1613
  superclassFieldName,
1261
1614
  };
1262
1615
  }
1263
1616
 
1617
+ private validateTablePrimaryKeyForMapping(
1618
+ tableRecord: TableInventoryRecord,
1619
+ tableColumns: DatasourceIntrospectionColumn[],
1620
+ mappingDto: DatasourceIntrospectionMappingDto,
1621
+ ) {
1622
+ const detectedPrimaryKeyColumns = tableColumns.filter((column) => column.isPrimaryKey);
1623
+ if (!detectedPrimaryKeyColumns.length) {
1624
+ throw new BadRequestException(tableRecord.mappingBlockedReason ?? this.buildMissingPrimaryKeyMessage(tableRecord.tableName));
1625
+ }
1626
+
1627
+ const requestColumnsByName = new Map(
1628
+ mappingDto.columns.map((column) => [column.columnName.trim().toLowerCase(), column]),
1629
+ );
1630
+
1631
+ for (const primaryKeyColumn of detectedPrimaryKeyColumns) {
1632
+ const matchingRequestColumn = requestColumnsByName.get(primaryKeyColumn.columnName.trim().toLowerCase());
1633
+ if (!matchingRequestColumn) {
1634
+ throw new BadRequestException(`Primary key column "${primaryKeyColumn.columnName}" must remain present in the mapping request for table "${tableRecord.tableName}".`);
1635
+ }
1636
+
1637
+ if (!matchingRequestColumn.include) {
1638
+ throw new BadRequestException(`Primary key column "${primaryKeyColumn.columnName}" cannot be excluded from the mapping for table "${tableRecord.tableName}".`);
1639
+ }
1640
+
1641
+ if (!matchingRequestColumn.isPrimaryKey) {
1642
+ throw new BadRequestException(`Primary key column "${primaryKeyColumn.columnName}" must stay marked as isPrimaryKey for table "${tableRecord.tableName}".`);
1643
+ }
1644
+ }
1645
+ }
1646
+
1647
+ private validateReviewedPrimaryKeyFields(
1648
+ reviewedFields: Array<Record<string, any>>,
1649
+ tableColumns: DatasourceIntrospectionColumn[],
1650
+ ) {
1651
+ const detectedPrimaryKeyColumns = tableColumns.filter((column) => column.isPrimaryKey);
1652
+ if (!detectedPrimaryKeyColumns.length) {
1653
+ return;
1654
+ }
1655
+
1656
+ const activeReviewedFields = reviewedFields.filter((field) => !field.isMarkedForRemoval);
1657
+
1658
+ for (const primaryKeyColumn of detectedPrimaryKeyColumns) {
1659
+ const matchingReviewedField = activeReviewedFields.find((field) =>
1660
+ `${field.columnName ?? ""}`.trim().toLowerCase() === primaryKeyColumn.columnName.trim().toLowerCase(),
1661
+ );
1662
+
1663
+ if (!matchingReviewedField) {
1664
+ throw new BadRequestException(`Reviewed metadata JSON must keep datasource primary key column "${primaryKeyColumn.columnName}" in the fields array.`);
1665
+ }
1666
+
1667
+ if (!matchingReviewedField.isPrimaryKey) {
1668
+ throw new BadRequestException(`Reviewed metadata JSON must keep datasource primary key column "${primaryKeyColumn.columnName}" marked with isPrimaryKey: true.`);
1669
+ }
1670
+ }
1671
+ }
1672
+
1673
+ private buildMissingPrimaryKeyMessage(tableName: string) {
1674
+ return `Table "${tableName}" cannot be mapped because the datasource table does not define any primary key. Add a real primary key to the table before continuing with SolidX mapping.`;
1675
+ }
1676
+
1264
1677
  private getSuperclassFieldName(columnName: string, legacyTableType: LegacyTableType) {
1265
1678
  return this.getLegacySuperclassFieldName(columnName, legacyTableType);
1266
1679
  }
@@ -1298,109 +1711,80 @@ export class DatasourceIntrospectionService {
1298
1711
  return "LegacyCommonEntityWithExistingId";
1299
1712
  }
1300
1713
 
1301
- private toSolidFieldType(column: DatasourceIntrospectionColumn) {
1302
- const normalized = column.dataType.toLowerCase();
1303
- const charLength = column.characterMaximumLength ?? null;
1304
-
1305
- if (["bit", "bool", "boolean"].includes(normalized)) {
1306
- return "boolean";
1307
- }
1308
-
1309
- if (["bigint"].includes(normalized)) {
1310
- return "bigint";
1311
- }
1714
+ private getDatasourceTypeFamily(dataType: string): DatasourceTypeFamily | null {
1715
+ const normalized = dataType.toLowerCase();
1312
1716
 
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";
1717
+ for (const [family, aliases] of Object.entries(DATASOURCE_TYPE_FAMILY_ALIASES) as Array<[DatasourceTypeFamily, string[]]>) {
1718
+ if (aliases.includes(normalized)) {
1719
+ return family;
1720
+ }
1323
1721
  }
1324
1722
 
1325
- if (["time"].includes(normalized)) {
1326
- return "time";
1723
+ if (normalized.includes("json")) {
1724
+ return "json";
1327
1725
  }
1328
1726
 
1329
- if (["datetime", "datetime2", "timestamp", "timestamptz", "smalldatetime"].includes(normalized) || normalized.includes("date")) {
1727
+ if (normalized.includes("date")) {
1330
1728
  return "datetime";
1331
1729
  }
1332
1730
 
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
- }
1731
+ return null;
1732
+ }
1340
1733
 
1341
- if (["text", "ntext", "mediumtext", "longtext", "clob"].includes(normalized)) {
1342
- return "longText";
1343
- }
1734
+ private toSolidFieldType(column: DatasourceIntrospectionColumn) {
1735
+ const family = this.getDatasourceTypeFamily(column.dataType);
1736
+ const charLength = column.characterMaximumLength ?? null;
1344
1737
 
1345
- if (["varchar", "nvarchar", "char", "nchar", "citext"].includes(normalized)) {
1346
- if (charLength !== null && charLength > 255) {
1738
+ switch (family) {
1739
+ case "boolean":
1740
+ return "boolean";
1741
+ case "bigint":
1742
+ return "bigint";
1743
+ case "int":
1744
+ return "int";
1745
+ case "numeric":
1746
+ case "decimal":
1747
+ case "float":
1748
+ return "decimal";
1749
+ case "date":
1750
+ return "date";
1751
+ case "time":
1752
+ return "time";
1753
+ case "datetime":
1754
+ case "datetimeWithTimezone":
1755
+ return "datetime";
1756
+ case "json":
1757
+ case "jsonb":
1758
+ return "json";
1759
+ case "uuid":
1760
+ return "uuid";
1761
+ case "mediumText":
1762
+ case "longText":
1347
1763
  return "longText";
1348
- }
1764
+ case "shortText":
1765
+ case "unicodeShortText":
1766
+ if (charLength !== null && charLength > 255) {
1767
+ return "longText";
1768
+ }
1349
1769
 
1350
- return "shortText";
1770
+ return "shortText";
1771
+ default:
1772
+ return "shortText";
1351
1773
  }
1352
-
1353
- return "shortText";
1354
1774
  }
1355
1775
 
1356
1776
  private toOrmFieldType(column: DatasourceIntrospectionColumn, datasourceType: string) {
1357
- const normalized = column.dataType.toLowerCase();
1777
+ const family = this.getDatasourceTypeFamily(column.dataType);
1358
1778
  const normalizedDatasourceType = datasourceType.toLowerCase();
1779
+ const ormType = family
1780
+ ? ORM_TYPE_BY_DATASOURCE_FAMILY[normalizedDatasourceType]?.[family]
1781
+ : undefined;
1782
+
1783
+ if (ormType) {
1784
+ return ormType;
1785
+ }
1359
1786
 
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";
1787
+ return normalizedDatasourceType === "postgres" ? "varchar" : "varchar";
1404
1788
  }
1405
1789
 
1406
1790
  private toFieldName(columnName: string) {
@@ -1449,6 +1833,285 @@ export class DatasourceIntrospectionService {
1449
1833
  return `${datasourceName.toLowerCase()}::${tableName.toLowerCase()}`;
1450
1834
  }
1451
1835
 
1836
+ private async syncTypeormDatasourceFile(moduleName: string, datasource: DatasourceRecord, singularName: string) {
1837
+ const datasourceFilePath = path.resolve(process.cwd(), "src", `typeorm-${datasource.name}-datasource.ts`);
1838
+ const entityClassName = classify(singularName);
1839
+ const entityImportPath = `./${moduleName}/entities/${kebabCase(singularName)}.entity`;
1840
+ const migrationGlob = `join(__dirname, './${moduleName}/migrations/${datasource.name}/*.{ts,js}')`;
1841
+
1842
+ const existingContent = await fs.readFile(datasourceFilePath, "utf8").catch(() => null);
1843
+ if (!existingContent) {
1844
+ const rendered = this.renderTypeormDatasourceTemplate({
1845
+ datasource,
1846
+ entityClassName,
1847
+ entityImportPath,
1848
+ migrationGlob,
1849
+ });
1850
+ await fs.writeFile(datasourceFilePath, rendered, "utf8");
1851
+ return {
1852
+ filePath: datasourceFilePath,
1853
+ created: true,
1854
+ updated: false,
1855
+ entityRegistered: true,
1856
+ migrationGlobRegistered: true,
1857
+ };
1858
+ }
1859
+
1860
+ const project = new Project({
1861
+ skipAddingFilesFromTsConfig: true,
1862
+ manipulationSettings: {
1863
+ quoteKind: QuoteKind.Single,
1864
+ indentationText: IndentationText.FourSpaces,
1865
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
1866
+ },
1867
+ });
1868
+ const sourceFile = project.createSourceFile(datasourceFilePath, existingContent, { overwrite: true });
1869
+
1870
+ const entityImportDeclaration = sourceFile.getImportDeclarations().find((declaration) =>
1871
+ declaration.getModuleSpecifierValue() === entityImportPath,
1872
+ );
1873
+ if (entityImportDeclaration) {
1874
+ const alreadyImported = entityImportDeclaration.getNamedImports().some((namedImport) => namedImport.getName() === entityClassName);
1875
+ if (!alreadyImported) {
1876
+ entityImportDeclaration.addNamedImport(entityClassName);
1877
+ }
1878
+ } else {
1879
+ sourceFile.addImportDeclaration({
1880
+ moduleSpecifier: entityImportPath,
1881
+ namedImports: [entityClassName],
1882
+ });
1883
+ }
1884
+
1885
+ const entitiesArray = sourceFile
1886
+ .getVariableDeclarationOrThrow("entities")
1887
+ .getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
1888
+ const entityAlreadyRegistered = this.arrayLiteralContainsValue(entitiesArray, entityClassName);
1889
+ if (!entityAlreadyRegistered) {
1890
+ entitiesArray.addElement(entityClassName);
1891
+ }
1892
+
1893
+ const dataSourceOptions = this.getTypeormDatasourceOptions(sourceFile);
1894
+ const migrationsArray = this.getObjectPropertyArrayLiteral(dataSourceOptions, "migrations");
1895
+ const migrationAlreadyRegistered = this.arrayLiteralContainsValue(migrationsArray, migrationGlob);
1896
+ if (!migrationAlreadyRegistered) {
1897
+ migrationsArray.addElement(migrationGlob);
1898
+ }
1899
+
1900
+ sourceFile.organizeImports();
1901
+ sourceFile.formatText();
1902
+
1903
+ const nextContent = sourceFile.getFullText();
1904
+ const updated = nextContent !== existingContent;
1905
+ if (updated) {
1906
+ await fs.writeFile(datasourceFilePath, nextContent, "utf8");
1907
+ }
1908
+
1909
+ return {
1910
+ filePath: datasourceFilePath,
1911
+ created: false,
1912
+ updated,
1913
+ entityRegistered: true,
1914
+ migrationGlobRegistered: true,
1915
+ };
1916
+ }
1917
+
1918
+ private async writeMigrationFile(migration: MappingPreviewRecord["migration"]) {
1919
+ if (!migration.willGenerate || !migration.content || !migration.filePath) {
1920
+ return false;
1921
+ }
1922
+
1923
+ await fs.mkdir(path.dirname(migration.filePath), { recursive: true });
1924
+ await fs.writeFile(migration.filePath, migration.content, "utf8");
1925
+
1926
+ const writtenContent = await fs.readFile(migration.filePath, "utf8").catch(() => null);
1927
+ if (writtenContent !== migration.content) {
1928
+ throw new BadRequestException(`Migration file could not be verified after writing: ${migration.filePath}`);
1929
+ }
1930
+
1931
+ return true;
1932
+ }
1933
+
1934
+ private renderTypeormDatasourceTemplate(input: {
1935
+ datasource: DatasourceRecord;
1936
+ entityClassName: string;
1937
+ entityImportPath: string;
1938
+ migrationGlob: string;
1939
+ }) {
1940
+ const { datasource, entityClassName, entityImportPath, migrationGlob } = input;
1941
+ const envPrefix = datasource.envPrefix?.toUpperCase?.() || "DEFAULT";
1942
+ const portFallback = datasource.type === "postgres" ? 5432 : datasource.type === "mysql" ? 3306 : 1433;
1943
+ const project = new Project({
1944
+ skipAddingFilesFromTsConfig: true,
1945
+ manipulationSettings: {
1946
+ quoteKind: QuoteKind.Single,
1947
+ indentationText: IndentationText.FourSpaces,
1948
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
1949
+ },
1950
+ });
1951
+ const sourceFile = project.createSourceFile("typeorm-template.ts", "", { overwrite: true });
1952
+
1953
+ sourceFile.addImportDeclaration({ moduleSpecifier: "reflect-metadata" });
1954
+ sourceFile.addImportDeclaration({ moduleSpecifier: "path", namedImports: ["join"] });
1955
+ sourceFile.addImportDeclaration({
1956
+ moduleSpecifier: "dotenv",
1957
+ namedImports: [{ name: "config", alias: "dotenvConfig" }],
1958
+ });
1959
+ sourceFile.addStatements("dotenvConfig({ path: join(__dirname, '../.env') });");
1960
+ sourceFile.addStatements("");
1961
+
1962
+ sourceFile.addImportDeclaration({ moduleSpecifier: "typeorm", namedImports: ["DataSource"] });
1963
+ sourceFile.addImportDeclaration({
1964
+ moduleSpecifier: "@solidxai/core",
1965
+ namespaceImport: "SolidCoreModuleExports",
1966
+ });
1967
+ sourceFile.addImportDeclaration({
1968
+ moduleSpecifier: "@solidxai/core",
1969
+ namedImports: ["getDynamicModuleNames", "parseBooleanEnv"],
1970
+ });
1971
+ sourceFile.addImportDeclaration({
1972
+ moduleSpecifier: "typeorm",
1973
+ namedImports: ["getMetadataArgsStorage"],
1974
+ });
1975
+ sourceFile.addImportDeclaration({
1976
+ moduleSpecifier: "typeorm-naming-strategies",
1977
+ namedImports: ["SnakeNamingStrategy"],
1978
+ });
1979
+ sourceFile.addImportDeclaration({
1980
+ moduleSpecifier: entityImportPath,
1981
+ namedImports: [entityClassName],
1982
+ });
1983
+
1984
+ sourceFile.addFunction({
1985
+ name: "getEntitiesFromExports",
1986
+ parameters: [{ name: "exports", type: "Record<string, any>" }],
1987
+ statements: (writer) => {
1988
+ writer.writeLine("const metadataStorage = getMetadataArgsStorage();");
1989
+ writer.writeLine("return Object.values(exports).filter((item) =>");
1990
+ writer.indent(() => {
1991
+ writer.writeLine("metadataStorage.tables.some((table) => table.target === item),");
1992
+ });
1993
+ writer.write(");");
1994
+ },
1995
+ });
1996
+
1997
+ sourceFile.addVariableStatement({
1998
+ declarationKind: VariableDeclarationKind.Const,
1999
+ declarations: [
2000
+ {
2001
+ name: "coreEntities",
2002
+ initializer: "getEntitiesFromExports(SolidCoreModuleExports)",
2003
+ },
2004
+ ],
2005
+ });
2006
+ sourceFile.addVariableStatement({
2007
+ declarationKind: VariableDeclarationKind.Const,
2008
+ declarations: [
2009
+ {
2010
+ name: "dynamicModules",
2011
+ initializer: "getDynamicModuleNames()",
2012
+ },
2013
+ ],
2014
+ });
2015
+ sourceFile.addVariableStatement({
2016
+ declarationKind: VariableDeclarationKind.Const,
2017
+ declarations: [
2018
+ {
2019
+ name: "entities",
2020
+ initializer: `[\n ...coreEntities,\n ${entityClassName},\n]`,
2021
+ },
2022
+ ],
2023
+ });
2024
+ sourceFile.addVariableStatement({
2025
+ declarationKind: VariableDeclarationKind.Const,
2026
+ declarations: [
2027
+ {
2028
+ name: "logging",
2029
+ initializer: `parseBooleanEnv('${envPrefix}_DATABASE_LOGGING')`,
2030
+ },
2031
+ ],
2032
+ });
2033
+
2034
+ sourceFile.addStatements("// IMPORTANT: synchronize must be false when using migrations");
2035
+ sourceFile.addVariableStatement({
2036
+ declarationKind: VariableDeclarationKind.Const,
2037
+ isExported: true,
2038
+ declarations: [
2039
+ {
2040
+ name: "DefaultDataSource",
2041
+ initializer: (writer) => {
2042
+ writer.write("new DataSource(");
2043
+ writer.inlineBlock(() => {
2044
+ writer.writeLine(`type: '${datasource.type}',`);
2045
+ writer.blankLine();
2046
+ writer.writeLine(`host: process.env.${envPrefix}_DATABASE_HOST,`);
2047
+ writer.writeLine(`port: +(process.env.${envPrefix}_DATABASE_PORT || ${portFallback}),`);
2048
+ writer.writeLine(`username: process.env.${envPrefix}_DATABASE_USER,`);
2049
+ writer.writeLine(`password: process.env.${envPrefix}_DATABASE_PASSWORD,`);
2050
+ writer.writeLine(`database: process.env.${envPrefix}_DATABASE_NAME,`);
2051
+ writer.blankLine();
2052
+ writer.writeLine("entities,");
2053
+ writer.writeLine("migrations: [");
2054
+ writer.indent(() => {
2055
+ writer.writeLine(`${migrationGlob},`);
2056
+ });
2057
+ writer.writeLine("],");
2058
+ writer.writeLine("synchronize: false,");
2059
+ writer.writeLine("logging,");
2060
+ writer.writeLine("namingStrategy: new SnakeNamingStrategy(),");
2061
+ if (datasource.type === "mssql") {
2062
+ writer.writeLine("options: {");
2063
+ writer.indent(() => {
2064
+ writer.writeLine("encrypt: false,");
2065
+ writer.writeLine("trustServerCertificate: true,");
2066
+ });
2067
+ writer.writeLine("},");
2068
+ }
2069
+ });
2070
+ writer.write(")");
2071
+ },
2072
+ },
2073
+ ],
2074
+ });
2075
+
2076
+ sourceFile.organizeImports();
2077
+ sourceFile.formatText();
2078
+ return sourceFile.getFullText();
2079
+ }
2080
+
2081
+ private arrayLiteralContainsValue(arrayLiteral: ArrayLiteralExpression, rawValue: string) {
2082
+ const normalizedTarget = rawValue.replace(/\s+/g, "");
2083
+ return arrayLiteral.getElements().some((element) => element.getText().replace(/\s+/g, "") === normalizedTarget);
2084
+ }
2085
+
2086
+ private getTypeormDatasourceOptions(sourceFile: import("ts-morph").SourceFile) {
2087
+ const defaultDataSourceDeclaration = sourceFile.getVariableDeclarationOrThrow("DefaultDataSource");
2088
+ const initializer = defaultDataSourceDeclaration.getInitializer();
2089
+ if (!initializer || !Node.isNewExpression(initializer)) {
2090
+ throw new BadRequestException("Unable to locate DefaultDataSource initializer in datasource file.");
2091
+ }
2092
+
2093
+ const options = initializer.getArguments()[0];
2094
+ if (!options || !Node.isObjectLiteralExpression(options)) {
2095
+ throw new BadRequestException("Unable to locate DefaultDataSource options object in datasource file.");
2096
+ }
2097
+
2098
+ return options;
2099
+ }
2100
+
2101
+ private getObjectPropertyArrayLiteral(objectLiteral: ObjectLiteralExpression, propertyName: string) {
2102
+ const property = objectLiteral.getProperty(propertyName);
2103
+ if (!property || !Node.isPropertyAssignment(property)) {
2104
+ throw new BadRequestException(`Unable to locate "${propertyName}" property in datasource file.`);
2105
+ }
2106
+
2107
+ const initializer = property.getInitializer();
2108
+ if (!initializer || !Node.isArrayLiteralExpression(initializer)) {
2109
+ throw new BadRequestException(`Unable to locate "${propertyName}" array in datasource file.`);
2110
+ }
2111
+
2112
+ return initializer;
2113
+ }
2114
+
1452
2115
  private buildSynchronizeBlockedMessage(datasource: DatasourceRecord) {
1453
2116
  return `Datasource "${datasource.displayName}" has synchronize enabled. Set ${datasource.envPrefix}_DATABASE_SYNCHRONIZE=false before starting legacy table introspection.`;
1454
2117
  }