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