@solidxai/core 0.1.11-beta.0 → 0.1.11-beta.11

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