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