@solidxai/core 0.1.10 → 0.1.11-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/controllers/datasource-introspection.controller.d.ts +234 -0
  2. package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -0
  3. package/dist/controllers/datasource-introspection.controller.js +98 -0
  4. package/dist/controllers/datasource-introspection.controller.js.map +1 -0
  5. package/dist/controllers/datasource-management.controller.d.ts +18 -0
  6. package/dist/controllers/datasource-management.controller.d.ts.map +1 -0
  7. package/dist/controllers/datasource-management.controller.js +55 -0
  8. package/dist/controllers/datasource-management.controller.js.map +1 -0
  9. package/dist/dtos/create-datasource-management.dto.d.ts +25 -0
  10. package/dist/dtos/create-datasource-management.dto.d.ts.map +1 -0
  11. package/dist/dtos/create-datasource-management.dto.js +169 -0
  12. package/dist/dtos/create-datasource-management.dto.js.map +1 -0
  13. package/dist/dtos/datasource-introspection-mapping.dto.d.ts +45 -0
  14. package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -0
  15. package/dist/dtos/datasource-introspection-mapping.dto.js +233 -0
  16. package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -0
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/interfaces/datasource-introspection-provider.interface.d.ts +26 -0
  22. package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -0
  23. package/dist/interfaces/datasource-introspection-provider.interface.js +3 -0
  24. package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -0
  25. package/dist/seeders/seed-data/solid-core-metadata.json +21 -0
  26. package/dist/services/authentication.service.js +1 -1
  27. package/dist/services/authentication.service.js.map +1 -1
  28. package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.d.ts.map +1 -1
  29. package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.js +2 -2
  30. package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.js.map +1 -1
  31. package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.d.ts.map +1 -1
  32. package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.js +2 -2
  33. package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.js.map +1 -1
  34. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.d.ts +3 -0
  35. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.d.ts.map +1 -1
  36. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.js +71 -0
  37. package/dist/services/dashboard-providers/mq-dashboard-provider-utils.js.map +1 -1
  38. package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.d.ts.map +1 -1
  39. package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.js +3 -3
  40. package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.js.map +1 -1
  41. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts +11 -0
  42. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -0
  43. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +181 -0
  44. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -0
  45. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts +11 -0
  46. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -0
  47. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +90 -0
  48. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -0
  49. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts +11 -0
  50. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -0
  51. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +156 -0
  52. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -0
  53. package/dist/services/datasource-introspection.service.d.ts +288 -0
  54. package/dist/services/datasource-introspection.service.d.ts.map +1 -0
  55. package/dist/services/datasource-introspection.service.js +1278 -0
  56. package/dist/services/datasource-introspection.service.js.map +1 -0
  57. package/dist/services/datasource-management.service.d.ts +60 -0
  58. package/dist/services/datasource-management.service.d.ts.map +1 -0
  59. package/dist/services/datasource-management.service.js +461 -0
  60. package/dist/services/datasource-management.service.js.map +1 -0
  61. package/dist/services/field-metadata.service.d.ts.map +1 -1
  62. package/dist/services/field-metadata.service.js +19 -0
  63. package/dist/services/field-metadata.service.js.map +1 -1
  64. package/dist/services/model-metadata.service.d.ts +1 -3
  65. package/dist/services/model-metadata.service.d.ts.map +1 -1
  66. package/dist/services/model-metadata.service.js +47 -15
  67. package/dist/services/model-metadata.service.js.map +1 -1
  68. package/dist/services/role-metadata.service.d.ts +1 -0
  69. package/dist/services/role-metadata.service.d.ts.map +1 -1
  70. package/dist/services/role-metadata.service.js +13 -0
  71. package/dist/services/role-metadata.service.js.map +1 -1
  72. package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.d.ts.map +1 -1
  73. package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.js +2 -3
  74. package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.js.map +1 -1
  75. package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.d.ts.map +1 -1
  76. package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.js +2 -3
  77. package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.js.map +1 -1
  78. package/dist/services/setting.service.d.ts +1 -0
  79. package/dist/services/setting.service.d.ts.map +1 -1
  80. package/dist/services/setting.service.js +32 -8
  81. package/dist/services/setting.service.js.map +1 -1
  82. package/dist/services/solid-ts-morph.service.d.ts +1 -1
  83. package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
  84. package/dist/services/solid-ts-morph.service.js.map +1 -1
  85. package/dist/solid-core.module.d.ts.map +1 -1
  86. package/dist/solid-core.module.js +16 -0
  87. package/dist/solid-core.module.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/controllers/datasource-introspection.controller.ts +58 -0
  90. package/src/controllers/datasource-management.controller.ts +24 -0
  91. package/src/dtos/create-datasource-management.dto.ts +139 -0
  92. package/src/dtos/datasource-introspection-mapping.dto.ts +182 -0
  93. package/src/index.ts +5 -0
  94. package/src/interfaces/datasource-introspection-provider.interface.ts +28 -0
  95. package/src/seeders/seed-data/solid-core-metadata.json +22 -1
  96. package/src/services/authentication.service.ts +1 -1
  97. package/src/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.ts +9 -4
  98. package/src/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.ts +9 -4
  99. package/src/services/dashboard-providers/mq-dashboard-provider-utils.ts +82 -0
  100. package/src/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.ts +10 -4
  101. package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +185 -0
  102. package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +95 -0
  103. package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +160 -0
  104. package/src/services/datasource-introspection.service.ts +1536 -0
  105. package/src/services/datasource-management.service.ts +566 -0
  106. package/src/services/field-metadata.service.ts +21 -1
  107. package/src/services/model-metadata.service.ts +50 -13
  108. package/src/services/role-metadata.service.ts +14 -0
  109. package/src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts +2 -4
  110. package/src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts +2 -4
  111. package/src/services/setting.service.ts +43 -12
  112. package/src/services/solid-ts-morph.service.ts +2 -2
  113. package/src/solid-core.module.ts +16 -0
@@ -0,0 +1,566 @@
1
+ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { CreateDatasourceManagementDto } from "src/dtos/create-datasource-management.dto";
5
+ import { DatasourceType } from "src/dtos/create-model-metadata.dto";
6
+ import { SolidTsMorphService } from "./solid-ts-morph.service";
7
+
8
+ export type DatasourceRecord = {
9
+ name: string;
10
+ displayName: string;
11
+ type: DatasourceType;
12
+ envPrefix: string;
13
+ isDefault: boolean;
14
+ host: string;
15
+ port: number | null;
16
+ database: string;
17
+ username: string;
18
+ passwordConfigured: boolean;
19
+ synchronize: boolean | null;
20
+ logging: boolean | null;
21
+ moduleFile: string;
22
+ envFile: string;
23
+ advanced: Record<string, any>;
24
+ };
25
+
26
+ type DatasourceAnnotations = {
27
+ displayName?: string;
28
+ envPrefix?: string;
29
+ advanced?: Record<string, any>;
30
+ };
31
+
32
+ @Injectable()
33
+ export class DatasourceManagementService {
34
+ private readonly logger = new Logger(DatasourceManagementService.name);
35
+
36
+ constructor(
37
+ private readonly solidTsMorphService: SolidTsMorphService,
38
+ ) { }
39
+
40
+ findMany() {
41
+ const records = this.listRecords();
42
+
43
+ return {
44
+ data: {
45
+ records,
46
+ meta: {
47
+ totalRecords: records.length,
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ listRecords() {
54
+ const solidApiRoot = this.resolveSolidApiRoot();
55
+ const envFilePath = join(solidApiRoot, ".env");
56
+ const envMap = this.parseEnvFile(envFilePath);
57
+ const datasourceFiles = this.getDatasourceModuleFiles(solidApiRoot);
58
+
59
+ return datasourceFiles
60
+ .map((moduleFilePath) => this.readDatasourceRecord(moduleFilePath, envMap, envFilePath))
61
+ .sort((left, right) => {
62
+ if (left.isDefault && !right.isDefault) return -1;
63
+ if (!left.isDefault && right.isDefault) return 1;
64
+ return left.displayName.localeCompare(right.displayName);
65
+ });
66
+ }
67
+
68
+ findOneByName(name: string) {
69
+ const normalizedName = name?.trim().toLowerCase();
70
+ if (!normalizedName) {
71
+ return null;
72
+ }
73
+
74
+ return this.listRecords().find((record) => record.name.toLowerCase() === normalizedName) ?? null;
75
+ }
76
+
77
+ async create(createDto: CreateDatasourceManagementDto) {
78
+ const solidApiRoot = this.resolveSolidApiRoot();
79
+ const datasourceName = createDto.name.trim().toLowerCase();
80
+ const envPrefix = this.toEnvPrefix(datasourceName);
81
+ const srcRoot = join(solidApiRoot, "src");
82
+ const moduleFilePath = join(srcRoot, `app-${datasourceName}-database.module.ts`);
83
+ const appModulePath = join(srcRoot, "app.module.ts");
84
+ const envFilePath = join(solidApiRoot, ".env");
85
+ const displayName = createDto.displayName?.trim() || this.toDisplayName(datasourceName);
86
+
87
+ if (datasourceName === "default") {
88
+ throw new BadRequestException('Datasource name "default" is reserved for the bootstrap datasource.');
89
+ }
90
+
91
+ if (existsSync(moduleFilePath)) {
92
+ throw new BadRequestException(`Datasource "${datasourceName}" already exists.`);
93
+ }
94
+
95
+ if (!existsSync(appModulePath)) {
96
+ throw new BadRequestException(`Unable to locate app.module.ts in ${solidApiRoot}.`);
97
+ }
98
+
99
+ const envMap = this.parseEnvFile(envFilePath);
100
+ const existingEnvKeys = Object.keys(envMap).filter((key) => key.startsWith(`${envPrefix}_DATABASE_`));
101
+ if (existingEnvKeys.length) {
102
+ throw new BadRequestException(
103
+ `Env prefix "${envPrefix}" is already in use. Remove the existing datasource variables before reusing this name.`,
104
+ );
105
+ }
106
+
107
+ const moduleClassName = `${this.toPascalCase(datasourceName)}DBModule`;
108
+ const advancedMetadata = this.buildAdvancedMetadata(createDto);
109
+ const moduleContent = this.renderDatasourceModule({
110
+ datasourceName,
111
+ displayName,
112
+ envPrefix,
113
+ moduleClassName,
114
+ type: createDto.type,
115
+ advancedMetadata,
116
+ });
117
+
118
+ this.solidTsMorphService.begin();
119
+ try {
120
+ this.solidTsMorphService.createNewFile(moduleFilePath, moduleContent, false);
121
+ this.solidTsMorphService.registerNestProvider(
122
+ appModulePath,
123
+ moduleClassName,
124
+ `./app-${datasourceName}-database.module`,
125
+ ["imports"],
126
+ );
127
+ await this.solidTsMorphService.commit();
128
+ } catch (error) {
129
+ this.solidTsMorphService.rollback();
130
+ this.logger.error(`Failed to create datasource "${datasourceName}"`, error as any);
131
+ throw error;
132
+ }
133
+
134
+ this.upsertManagedEnvBlock(
135
+ envFilePath,
136
+ datasourceName,
137
+ this.renderEnvBlock(createDto, envPrefix),
138
+ );
139
+
140
+ const refreshedEnvMap = this.parseEnvFile(envFilePath);
141
+ return {
142
+ data: this.readDatasourceRecord(moduleFilePath, refreshedEnvMap, envFilePath),
143
+ };
144
+ }
145
+
146
+ private resolveSolidApiRoot() {
147
+ const cwd = process.cwd();
148
+ const candidates = [
149
+ cwd,
150
+ join(cwd, "solid-api"),
151
+ ];
152
+
153
+ for (const candidate of candidates) {
154
+ if (existsSync(join(candidate, "src", "app.module.ts"))) {
155
+ return candidate;
156
+ }
157
+ }
158
+
159
+ throw new BadRequestException(
160
+ "Datasource management expects to run inside a SolidX consuming project with a solid-api workspace.",
161
+ );
162
+ }
163
+
164
+ private getDatasourceModuleFiles(solidApiRoot: string) {
165
+ const srcRoot = join(solidApiRoot, "src");
166
+ return readdirSync(srcRoot)
167
+ .filter((fileName) => /^app-[a-z0-9-]+-database\.module\.ts$/.test(fileName))
168
+ .map((fileName) => join(srcRoot, fileName));
169
+ }
170
+
171
+ private readDatasourceRecord(
172
+ moduleFilePath: string,
173
+ envMap: Record<string, string>,
174
+ envFilePath: string,
175
+ ): DatasourceRecord {
176
+ const content = readFileSync(moduleFilePath, "utf8");
177
+ const fileName = basename(moduleFilePath);
178
+ const fileNameMatch = fileName.match(/^app-(.+)-database\.module\.ts$/);
179
+ const inferredName = fileNameMatch?.[1] ?? "unknown";
180
+ const datasourceName = this.matchFirst(
181
+ content,
182
+ /name\(\)\s*:\s*string\s*{\s*return\s+['"]([^'"]+)['"]/s,
183
+ ) ?? inferredName;
184
+ const annotations = this.parseAnnotations(content);
185
+ const envPrefix = annotations.envPrefix || this.toEnvPrefix(datasourceName);
186
+ const envType = envMap[`${envPrefix}_DATABASE_TYPE`];
187
+ const inferredType =
188
+ this.matchFirst(content, /type\(\)\s*:\s*DatasourceType\s*{\s*return\s+DatasourceType\.([a-zA-Z0-9_]+)/s)
189
+ ?? this.matchFirst(content, /type:\s*['"]([a-zA-Z0-9_]+)['"]/s)
190
+ ?? DatasourceType.postgres;
191
+ const type = (envType as DatasourceType) || (inferredType as DatasourceType);
192
+ const advancedFromEnv = this.readAdvancedEnvValues(envPrefix, envMap, type);
193
+
194
+ return {
195
+ name: datasourceName,
196
+ displayName: annotations.displayName || this.toDisplayName(datasourceName),
197
+ type,
198
+ envPrefix,
199
+ isDefault: datasourceName === "default",
200
+ host: envMap[`${envPrefix}_DATABASE_HOST`] ?? "",
201
+ port: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_PORT`]),
202
+ database: envMap[`${envPrefix}_DATABASE_NAME`] ?? "",
203
+ username: envMap[`${envPrefix}_DATABASE_USER`] ?? "",
204
+ passwordConfigured: Boolean(envMap[`${envPrefix}_DATABASE_PASSWORD`]),
205
+ synchronize: this.toBooleanOrNull(envMap[`${envPrefix}_DATABASE_SYNCHRONIZE`]),
206
+ logging: this.toBooleanOrNull(envMap[`${envPrefix}_DATABASE_LOGGING`]),
207
+ moduleFile: moduleFilePath,
208
+ envFile: envFilePath,
209
+ advanced: {
210
+ ...(annotations.advanced ?? {}),
211
+ ...advancedFromEnv,
212
+ },
213
+ };
214
+ }
215
+
216
+ private parseAnnotations(content: string): DatasourceAnnotations {
217
+ const displayName = this.matchFirst(
218
+ content,
219
+ /^\s*\*\s*@solidxDatasource\.displayName\s+(.+)$/m,
220
+ );
221
+ const envPrefix = this.matchFirst(
222
+ content,
223
+ /^\s*\*\s*@solidxDatasource\.envPrefix\s+([A-Z0-9_]+)$/m,
224
+ );
225
+ const advancedText = this.matchFirst(
226
+ content,
227
+ /^\s*\*\s*@solidxDatasource\.advanced\s+(.+)$/m,
228
+ );
229
+
230
+ let advanced: Record<string, any> | undefined;
231
+ if (advancedText) {
232
+ try {
233
+ advanced = JSON.parse(advancedText);
234
+ } catch (error) {
235
+ this.logger.warn(`Unable to parse datasource annotation JSON in module file: ${error}`);
236
+ }
237
+ }
238
+
239
+ return {
240
+ displayName: displayName?.trim(),
241
+ envPrefix: envPrefix?.trim(),
242
+ advanced,
243
+ };
244
+ }
245
+
246
+ private parseEnvFile(envFilePath: string) {
247
+ if (!existsSync(envFilePath)) {
248
+ return {};
249
+ }
250
+
251
+ const content = readFileSync(envFilePath, "utf8");
252
+ const envMap: Record<string, string> = {};
253
+
254
+ content.split(/\r?\n/).forEach((line) => {
255
+ const trimmed = line.trim();
256
+ if (!trimmed || trimmed.startsWith("#")) {
257
+ return;
258
+ }
259
+
260
+ const normalized = trimmed.startsWith("export ") ? trimmed.slice(7) : trimmed;
261
+ const separatorIndex = normalized.indexOf("=");
262
+ if (separatorIndex === -1) {
263
+ return;
264
+ }
265
+
266
+ const key = normalized.slice(0, separatorIndex).trim();
267
+ const rawValue = normalized.slice(separatorIndex + 1).trim();
268
+ envMap[key] = this.stripWrappingQuotes(rawValue);
269
+ });
270
+
271
+ return envMap;
272
+ }
273
+
274
+ private stripWrappingQuotes(value: string) {
275
+ if (
276
+ (value.startsWith('"') && value.endsWith('"'))
277
+ || (value.startsWith("'") && value.endsWith("'"))
278
+ ) {
279
+ return value.slice(1, -1);
280
+ }
281
+ return value;
282
+ }
283
+
284
+ private upsertManagedEnvBlock(envFilePath: string, datasourceName: string, blockBody: string) {
285
+ const startMarker = `# >>> solidx-datasource:${datasourceName}`;
286
+ const endMarker = `# <<< solidx-datasource:${datasourceName}`;
287
+ const managedBlock = `${startMarker}\n${blockBody}\n${endMarker}`;
288
+ const escapedStart = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
289
+ const escapedEnd = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
+ const blockPattern = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`, "m");
291
+ const existingContent = existsSync(envFilePath) ? readFileSync(envFilePath, "utf8") : "";
292
+ let nextContent = "";
293
+
294
+ if (blockPattern.test(existingContent)) {
295
+ nextContent = existingContent.replace(blockPattern, managedBlock);
296
+ } else if (!existingContent.trim()) {
297
+ nextContent = `${managedBlock}\n`;
298
+ } else {
299
+ const suffix = existingContent.endsWith("\n") ? "" : "\n";
300
+ nextContent = `${existingContent}${suffix}\n${managedBlock}\n`;
301
+ }
302
+
303
+ writeFileSync(envFilePath, nextContent, "utf8");
304
+ }
305
+
306
+ private renderEnvBlock(dto: CreateDatasourceManagementDto, envPrefix: string) {
307
+ const lines = [
308
+ `${envPrefix}_DATABASE_TYPE=${dto.type}`,
309
+ `${envPrefix}_DATABASE_HOST=${dto.host}`,
310
+ `${envPrefix}_DATABASE_PORT=${dto.port}`,
311
+ `${envPrefix}_DATABASE_USER=${dto.username}`,
312
+ `${envPrefix}_DATABASE_PASSWORD=${dto.password}`,
313
+ `${envPrefix}_DATABASE_NAME=${dto.database}`,
314
+ `${envPrefix}_DATABASE_SYNCHRONIZE=${dto.synchronize ?? false}`,
315
+ `${envPrefix}_DATABASE_LOGGING=${dto.logging ?? false}`,
316
+ ];
317
+
318
+ const optionalValues: Array<[string, string | number | boolean | undefined]> = [
319
+ [`${envPrefix}_DATABASE_SSL`, dto.ssl],
320
+ [`${envPrefix}_DATABASE_SSL_REJECT_UNAUTHORIZED`, dto.sslRejectUnauthorized],
321
+ [`${envPrefix}_DATABASE_POOL_MAX`, dto.poolMax],
322
+ [`${envPrefix}_DATABASE_CONNECTION_TIMEOUT_MS`, dto.connectionTimeoutMs],
323
+ [`${envPrefix}_DATABASE_IDLE_TIMEOUT_MS`, dto.idleTimeoutMs],
324
+ [`${envPrefix}_DATABASE_STATEMENT_TIMEOUT_MS`, dto.statementTimeoutMs],
325
+ [`${envPrefix}_DATABASE_IDLE_IN_TX_TIMEOUT_MS`, dto.idleInTxTimeoutMs],
326
+ [`${envPrefix}_DATABASE_RETRY_ATTEMPTS`, dto.retryAttempts],
327
+ [`${envPrefix}_DATABASE_RETRY_DELAY_MS`, dto.retryDelayMs],
328
+ [`${envPrefix}_DATABASE_ENCRYPT`, dto.encrypt],
329
+ [`${envPrefix}_DATABASE_TRUST_SERVER_CERTIFICATE`, dto.trustServerCertificate],
330
+ ];
331
+
332
+ optionalValues.forEach(([key, value]) => {
333
+ if (value === undefined || value === null || value === "") {
334
+ return;
335
+ }
336
+ lines.push(`${key}=${value}`);
337
+ });
338
+
339
+ return lines.join("\n");
340
+ }
341
+
342
+ private renderDatasourceModule(args: {
343
+ datasourceName: string;
344
+ displayName: string;
345
+ envPrefix: string;
346
+ moduleClassName: string;
347
+ type: DatasourceType;
348
+ advancedMetadata: Record<string, any>;
349
+ }) {
350
+ const defaultPort = this.getDefaultPort(args.type);
351
+ const providerSpecificFactoryLines = this.renderProviderSpecificFactoryLines(args.envPrefix, args.type);
352
+ const providerSpecificAdvancedLines = this.renderProviderSpecificAdvancedLines(args.envPrefix, args.type);
353
+ const advancedJson = JSON.stringify(args.advancedMetadata);
354
+
355
+ return `import { Module } from "@nestjs/common";
356
+ import { TypeOrmModule } from "@nestjs/typeorm";
357
+ import {
358
+ DatasourceType,
359
+ ISolidDatabaseModule,
360
+ parseBooleanEnv,
361
+ SolidDatabaseModule,
362
+ WinstonTypeORMLogger,
363
+ } from "@solidxai/core";
364
+ import { WINSTON_MODULE_PROVIDER } from "nest-winston";
365
+ import { SnakeNamingStrategy } from "typeorm-naming-strategies";
366
+ import { Logger } from "winston";
367
+
368
+ @Module({
369
+ imports: [
370
+ TypeOrmModule.forRootAsync({
371
+ name: "${args.datasourceName}",
372
+ useFactory: (logger: Logger) => {
373
+ const datasourceType = (process.env.${args.envPrefix}_DATABASE_TYPE || "${args.type}") as DatasourceType;
374
+ const synchronize = parseBooleanEnv("${args.envPrefix}_DATABASE_SYNCHRONIZE");
375
+ const logging = parseBooleanEnv("${args.envPrefix}_DATABASE_LOGGING");
376
+
377
+ return {
378
+ name: "${args.datasourceName}",
379
+ type: datasourceType,
380
+ host: process.env.${args.envPrefix}_DATABASE_HOST,
381
+ port: +(process.env.${args.envPrefix}_DATABASE_PORT || ${defaultPort}),
382
+ username: process.env.${args.envPrefix}_DATABASE_USER,
383
+ password: process.env.${args.envPrefix}_DATABASE_PASSWORD,
384
+ database: process.env.${args.envPrefix}_DATABASE_NAME,
385
+ // Entities are attached incrementally through TypeOrmModule.forFeature(..., "${args.datasourceName}").
386
+ autoLoadEntities: true,
387
+ synchronize,
388
+ logging,
389
+ logger: logging ? new WinstonTypeORMLogger(logger) : undefined,
390
+ namingStrategy: new SnakeNamingStrategy(),
391
+ maxQueryExecutionTime: 500,
392
+ ${providerSpecificFactoryLines}
393
+ ${providerSpecificAdvancedLines}
394
+ retryAttempts: Number(process.env.${args.envPrefix}_DATABASE_RETRY_ATTEMPTS ?? 0),
395
+ retryDelay: Number(process.env.${args.envPrefix}_DATABASE_RETRY_DELAY_MS ?? 0),
396
+ };
397
+ },
398
+ inject: [WINSTON_MODULE_PROVIDER],
399
+ }),
400
+ ],
401
+ })
402
+ @SolidDatabaseModule()
403
+ /**
404
+ * @solidxDatasource.displayName ${args.displayName}
405
+ * @solidxDatasource.envPrefix ${args.envPrefix}
406
+ * @solidxDatasource.advanced ${advancedJson}
407
+ */
408
+ export class ${args.moduleClassName} implements ISolidDatabaseModule {
409
+ type(): DatasourceType {
410
+ return DatasourceType.${args.type};
411
+ }
412
+
413
+ name(): string {
414
+ return "${args.datasourceName}";
415
+ }
416
+ }
417
+ `;
418
+ }
419
+
420
+ private renderProviderSpecificFactoryLines(envPrefix: string, type: DatasourceType) {
421
+ if (type === DatasourceType.mssql) {
422
+ return ` options: {
423
+ encrypt: parseBooleanEnv("${envPrefix}_DATABASE_ENCRYPT"),
424
+ trustServerCertificate: parseBooleanEnv("${envPrefix}_DATABASE_TRUST_SERVER_CERTIFICATE"),
425
+ useUTC: true,
426
+ },`;
427
+ }
428
+
429
+ return ` ssl: parseBooleanEnv("${envPrefix}_DATABASE_SSL")
430
+ ? {
431
+ rejectUnauthorized: process.env.${envPrefix}_DATABASE_SSL_REJECT_UNAUTHORIZED !== "false",
432
+ }
433
+ : false,`;
434
+ }
435
+
436
+ private renderProviderSpecificAdvancedLines(envPrefix: string, type: DatasourceType) {
437
+ if (type === DatasourceType.postgres) {
438
+ return ` extra: {
439
+ max: Number(process.env.${envPrefix}_DATABASE_POOL_MAX ?? 20),
440
+ connectionTimeoutMillis: Number(process.env.${envPrefix}_DATABASE_CONNECTION_TIMEOUT_MS ?? 60000),
441
+ idleTimeoutMillis: Number(process.env.${envPrefix}_DATABASE_IDLE_TIMEOUT_MS ?? 30000),
442
+ statement_timeout: Number(process.env.${envPrefix}_DATABASE_STATEMENT_TIMEOUT_MS ?? 60000),
443
+ idle_in_transaction_session_timeout: Number(process.env.${envPrefix}_DATABASE_IDLE_IN_TX_TIMEOUT_MS ?? 60000),
444
+ },`;
445
+ }
446
+
447
+ return ` extra: {
448
+ max: Number(process.env.${envPrefix}_DATABASE_POOL_MAX ?? 20),
449
+ connectionTimeoutMillis: Number(process.env.${envPrefix}_DATABASE_CONNECTION_TIMEOUT_MS ?? 30000),
450
+ idleTimeoutMillis: Number(process.env.${envPrefix}_DATABASE_IDLE_TIMEOUT_MS ?? 30000),
451
+ },`;
452
+ }
453
+
454
+ private buildAdvancedMetadata(dto: CreateDatasourceManagementDto) {
455
+ return this.compactObject({
456
+ synchronize: dto.synchronize ?? false,
457
+ logging: dto.logging ?? false,
458
+ ssl: dto.ssl,
459
+ sslRejectUnauthorized: dto.sslRejectUnauthorized,
460
+ poolMax: dto.poolMax,
461
+ connectionTimeoutMs: dto.connectionTimeoutMs,
462
+ idleTimeoutMs: dto.idleTimeoutMs,
463
+ statementTimeoutMs: dto.statementTimeoutMs,
464
+ idleInTxTimeoutMs: dto.idleInTxTimeoutMs,
465
+ retryAttempts: dto.retryAttempts,
466
+ retryDelayMs: dto.retryDelayMs,
467
+ encrypt: dto.encrypt,
468
+ trustServerCertificate: dto.trustServerCertificate,
469
+ });
470
+ }
471
+
472
+ private readAdvancedEnvValues(
473
+ envPrefix: string,
474
+ envMap: Record<string, string>,
475
+ type: DatasourceType,
476
+ ) {
477
+ const advanced: Record<string, any> = this.compactObject({
478
+ ssl: this.toBooleanOrNull(envMap[`${envPrefix}_DATABASE_SSL`]),
479
+ sslRejectUnauthorized: this.toBooleanOrNull(envMap[`${envPrefix}_DATABASE_SSL_REJECT_UNAUTHORIZED`]),
480
+ poolMax: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_POOL_MAX`]),
481
+ connectionTimeoutMs: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_CONNECTION_TIMEOUT_MS`]),
482
+ idleTimeoutMs: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_IDLE_TIMEOUT_MS`]),
483
+ statementTimeoutMs: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_STATEMENT_TIMEOUT_MS`]),
484
+ idleInTxTimeoutMs: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_IDLE_IN_TX_TIMEOUT_MS`]),
485
+ retryAttempts: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_RETRY_ATTEMPTS`]),
486
+ retryDelayMs: this.toNumberOrNull(envMap[`${envPrefix}_DATABASE_RETRY_DELAY_MS`]),
487
+ encrypt: this.toBooleanOrNull(envMap[`${envPrefix}_DATABASE_ENCRYPT`]),
488
+ trustServerCertificate: this.toBooleanOrNull(envMap[`${envPrefix}_DATABASE_TRUST_SERVER_CERTIFICATE`]),
489
+ });
490
+
491
+ if (type !== DatasourceType.mssql) {
492
+ delete advanced.encrypt;
493
+ delete advanced.trustServerCertificate;
494
+ }
495
+
496
+ if (type !== DatasourceType.postgres) {
497
+ delete advanced.statementTimeoutMs;
498
+ delete advanced.idleInTxTimeoutMs;
499
+ }
500
+
501
+ return advanced;
502
+ }
503
+
504
+ private compactObject(input: Record<string, any>) {
505
+ return Object.fromEntries(
506
+ Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""),
507
+ );
508
+ }
509
+
510
+ private getDefaultPort(type: DatasourceType) {
511
+ switch (type) {
512
+ case DatasourceType.mysql:
513
+ return 3306;
514
+ case DatasourceType.mssql:
515
+ return 1433;
516
+ case DatasourceType.postgres:
517
+ default:
518
+ return 5432;
519
+ }
520
+ }
521
+
522
+ private toEnvPrefix(name: string) {
523
+ return name.replace(/-/g, "_").toUpperCase();
524
+ }
525
+
526
+ private toDisplayName(name: string) {
527
+ return name
528
+ .split(/[-_]+/)
529
+ .filter(Boolean)
530
+ .map((token) => token.charAt(0).toUpperCase() + token.slice(1))
531
+ .join(" ");
532
+ }
533
+
534
+ private toPascalCase(name: string) {
535
+ return name
536
+ .split(/[-_]+/)
537
+ .filter(Boolean)
538
+ .map((token) => token.charAt(0).toUpperCase() + token.slice(1))
539
+ .join("");
540
+ }
541
+
542
+ private matchFirst(content: string, regex: RegExp) {
543
+ return content.match(regex)?.[1];
544
+ }
545
+
546
+ private toBooleanOrNull(value?: string) {
547
+ if (value === undefined) {
548
+ return null;
549
+ }
550
+ if (value === "true") {
551
+ return true;
552
+ }
553
+ if (value === "false") {
554
+ return false;
555
+ }
556
+ return null;
557
+ }
558
+
559
+ private toNumberOrNull(value?: string) {
560
+ if (value === undefined || value === "") {
561
+ return null;
562
+ }
563
+ const parsed = Number(value);
564
+ return Number.isFinite(parsed) ? parsed : null;
565
+ }
566
+ }
@@ -1161,6 +1161,27 @@ export class FieldMetadataService implements OnApplicationBootstrap {
1161
1161
 
1162
1162
  // if found
1163
1163
  if (existingFieldMetadata) {
1164
+ const hasChanges = Object.entries(updateDto).some(([key, value]) => {
1165
+ if (key === 'model') {
1166
+ return existingFieldMetadata.model?.id !== value?.id;
1167
+ }
1168
+ if (key === 'mediaStorageProvider') {
1169
+ return existingFieldMetadata.mediaStorageProvider?.id !== value?.id;
1170
+ }
1171
+ const currentValue = existingFieldMetadata[key];
1172
+ if (Array.isArray(currentValue) || Array.isArray(value)) {
1173
+ return JSON.stringify(currentValue ?? null) !== JSON.stringify(value ?? null);
1174
+ }
1175
+ if (value && typeof value === 'object') {
1176
+ return JSON.stringify(currentValue ?? null) !== JSON.stringify(value ?? null);
1177
+ }
1178
+ return currentValue !== value;
1179
+ });
1180
+
1181
+ if (!hasChanges) {
1182
+ return existingFieldMetadata;
1183
+ }
1184
+
1164
1185
  const updatedFieldMetadata = { ...existingFieldMetadata, ...updateDto };
1165
1186
  return this.fieldMetadataRepo.save(updatedFieldMetadata);
1166
1187
  }
@@ -1322,4 +1343,3 @@ export class FieldMetadataService implements OnApplicationBootstrap {
1322
1343
  }
1323
1344
  }
1324
1345
  }
1325
-