@solidxai/core 0.1.14 → 0.1.15-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.
Files changed (42) hide show
  1. package/CHANGELOG.md +1197 -0
  2. package/CLAUDE.md +26 -0
  3. package/dist/constants/media-file-types.d.ts +1 -0
  4. package/dist/constants/media-file-types.d.ts.map +1 -1
  5. package/dist/constants/media-file-types.js +10 -1
  6. package/dist/constants/media-file-types.js.map +1 -1
  7. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  8. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +5 -5
  9. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  10. package/dist/helpers/user-helper.d.ts.map +1 -1
  11. package/dist/helpers/user-helper.js +2 -0
  12. package/dist/helpers/user-helper.js.map +1 -1
  13. package/dist/seeders/seed-data/solid-core-metadata.json +0 -2
  14. package/dist/services/import-transaction.service.d.ts +1 -0
  15. package/dist/services/import-transaction.service.d.ts.map +1 -1
  16. package/dist/services/import-transaction.service.js +21 -0
  17. package/dist/services/import-transaction.service.js.map +1 -1
  18. package/dist/services/model-metadata.service.d.ts +0 -1
  19. package/dist/services/model-metadata.service.d.ts.map +1 -1
  20. package/dist/services/model-metadata.service.js +0 -13
  21. package/dist/services/model-metadata.service.js.map +1 -1
  22. package/dist/solid-core.module.d.ts.map +1 -1
  23. package/dist/solid-core.module.js +0 -4
  24. package/dist/solid-core.module.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/constants/media-file-types.ts +18 -2
  27. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +14 -6
  28. package/src/helpers/user-helper.ts +2 -0
  29. package/src/seeders/seed-data/solid-core-metadata.json +0 -2
  30. package/src/services/import-transaction.service.ts +35 -0
  31. package/src/services/model-metadata.service.ts +0 -25
  32. package/src/solid-core.module.ts +0 -4
  33. package/dist/commands/migrate-removed-fields.command.d.ts +0 -19
  34. package/dist/commands/migrate-removed-fields.command.d.ts.map +0 -1
  35. package/dist/commands/migrate-removed-fields.command.js +0 -77
  36. package/dist/commands/migrate-removed-fields.command.js.map +0 -1
  37. package/dist/services/removed-field-migration.service.d.ts +0 -30
  38. package/dist/services/removed-field-migration.service.d.ts.map +0 -1
  39. package/dist/services/removed-field-migration.service.js +0 -319
  40. package/dist/services/removed-field-migration.service.js.map +0 -1
  41. package/src/commands/migrate-removed-fields.command.ts +0 -81
  42. package/src/services/removed-field-migration.service.ts +0 -334
@@ -1,81 +0,0 @@
1
- import { Logger } from "@nestjs/common";
2
- import { Command, CommandRunner, Option } from "nest-commander";
3
- import { RemovedFieldMigrationService } from "src/services/removed-field-migration.service";
4
- import { CommandError } from "./helper";
5
- import { ModelMetadataService } from "src/services/model-metadata.service";
6
-
7
- interface CommandOptions {
8
- name: string;
9
- dryRun?: boolean;
10
- }
11
-
12
- @Command({
13
- name: "migrate-removed-fields",
14
- description: "Drops live database artifacts for fields marked for removal and cleans the related metadata.",
15
- })
16
- export class MigrateRemovedFieldsCommand extends CommandRunner {
17
- constructor(
18
- private readonly removedFieldMigrationService: RemovedFieldMigrationService,
19
- private readonly modelMetadataService: ModelMetadataService,
20
-
21
- ) {
22
- super();
23
- }
24
-
25
- private readonly logger = new Logger(MigrateRemovedFieldsCommand.name);
26
-
27
- async run(_passedParam: string[], options?: CommandOptions): Promise<void> {
28
- const errors = this.validate(options);
29
- if (errors.length) {
30
- errors.forEach((error) => this.logger.error(error));
31
- return;
32
- }
33
-
34
- const dryRun = options?.dryRun ?? true;
35
-
36
- // STEP 1: Capture fields BEFORE migration deletes metadata
37
- // const model = await this.modelMetadataService.findOneByUserKey(
38
- // options.name,
39
- // ["module", "fields"],
40
- // );
41
-
42
- // const fieldsForRemoval = model.fields.filter(
43
- // field => field.isMarkedForRemoval,
44
- // );
45
-
46
- // // STEP 2: Run remove-fields schematic first
47
- // if (!dryRun && fieldsForRemoval.length > 0) {
48
- // // await this.modelMetadataService.executeRemoveFieldsOnly(options.name,fieldsForRemoval.map(f => f.name),false,);
49
- // await this.modelMetadataService.executeRemoveFieldsWithModel(model, fieldsForRemoval.map(f => f.name),false,);
50
- // }
51
-
52
- // STEP 3: Then perform DB + metadata cleanup
53
- const result = await this.removedFieldMigrationService.migrateMarkedFields(options.name, dryRun,);
54
-
55
- result.operations.forEach((operation) => this.logger.log(operation));
56
- }
57
-
58
- @Option({
59
- flags: "-n, --name <model name>",
60
- description: "Model name (singularName) from the ss_model_metadata table",
61
- })
62
- parseName(val: string): string {
63
- return val;
64
- }
65
-
66
- @Option({
67
- flags: "-d, --dryRun [dry run]",
68
- description: "Dry run the command",
69
- })
70
- parseDryRun(val: string): boolean {
71
- this.logger.debug(`Dry run : ${val}`);
72
- return val === "false" ? false : true;
73
- }
74
-
75
- private validate(options: CommandOptions): CommandError[] {
76
- if (!options?.name) {
77
- return [new CommandError("Model Name is required")];
78
- }
79
- return [];
80
- }
81
- }
@@ -1,334 +0,0 @@
1
- import * as fs from "fs/promises";
2
- import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
3
- import { ModuleRef } from "@nestjs/core";
4
- import { getDataSourceToken } from "@nestjs/typeorm";
5
- import { kebabCase, snakeCase } from "lodash";
6
- import * as path from "path";
7
- import { ERROR_MESSAGES } from "src/constants/error-messages";
8
- import { RelationType, SolidFieldType } from "src/dtos/create-field-metadata.dto";
9
- import { FieldMetadata } from "src/entities/field-metadata.entity";
10
- import { ModelMetadata } from "src/entities/model-metadata.entity";
11
- import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service";
12
- import { classify } from "src/helpers/string.helper";
13
- import { FieldMetadataRepository } from "src/repository/field-metadata.repository";
14
- import { ModelMetadataRepository } from "src/repository/model-metadata.repository";
15
- import { DataSource, EntityMetadata, QueryRunner, Table } from "typeorm";
16
-
17
- export interface RemovedFieldMigrationResult {
18
- dryRun: boolean;
19
- modelName: string;
20
- operations: string[];
21
- removedFieldNames: string[];
22
- }
23
-
24
- @Injectable()
25
- export class RemovedFieldMigrationService {
26
- constructor(
27
- private readonly modelMetadataRepo: ModelMetadataRepository,
28
- private readonly fieldMetadataRepo: FieldMetadataRepository,
29
- private readonly moduleMetadataHelperService: ModuleMetadataHelperService,
30
- private readonly moduleRef: ModuleRef,
31
- ) { }
32
-
33
- private readonly logger = new Logger(RemovedFieldMigrationService.name);
34
-
35
- // Cleans fields marked for removal by updating schema state and metadata for a single model.
36
- async migrateMarkedFields(modelUserKey: string, dryRun: boolean = false): Promise<RemovedFieldMigrationResult> {
37
- if (!modelUserKey) {
38
- throw new BadRequestException("Model name is required");
39
- }
40
-
41
- const model = await this.modelMetadataRepo.findOne({
42
- where: { singularName: modelUserKey },
43
- relations: { fields: true, module: true },
44
- });
45
-
46
- if (!model) {
47
- throw new NotFoundException(ERROR_MESSAGES.MODEL_NOT_FOUND(modelUserKey));
48
- }
49
-
50
- const fieldsForRemoval = model.fields.filter((field) => field.isMarkedForRemoval);
51
- const operations: string[] = [];
52
-
53
- if (fieldsForRemoval.length === 0) {
54
- const message = `No fields marked for removal were found for model "${model.singularName}".`;
55
- this.logger.log(message);
56
- operations.push(message);
57
- return {
58
- dryRun,
59
- modelName: model.singularName,
60
- operations,
61
- removedFieldNames: [],
62
- };
63
- }
64
-
65
- const dataSource = await this.resolveDataSource(model.dataSource);
66
- const entityMetadata = this.resolveEntityMetadata(dataSource, model);
67
- const queryRunner = dataSource.createQueryRunner();
68
-
69
- try {
70
- await queryRunner.connect();
71
-
72
- if (!dryRun) {
73
- await queryRunner.startTransaction();
74
- }
75
-
76
- for (const field of fieldsForRemoval) {
77
- await this.cleanupMarkedField({
78
- field,
79
- model,
80
- entityMetadata,
81
- queryRunner,
82
- dryRun,
83
- operations,
84
- });
85
- }
86
-
87
- if (!dryRun) {
88
- await queryRunner.commitTransaction();
89
- }
90
- } catch (error) {
91
- if (!dryRun && queryRunner.isTransactionActive) {
92
- await queryRunner.rollbackTransaction();
93
- }
94
- throw error;
95
- } finally {
96
- await queryRunner.release();
97
- }
98
-
99
- return {
100
- dryRun,
101
- modelName: model.singularName,
102
- operations,
103
- removedFieldNames: fieldsForRemoval.map((field) => field.name),
104
- };
105
- }
106
-
107
- private async cleanupMarkedField(params: { field: FieldMetadata; model: ModelMetadata; entityMetadata?: EntityMetadata; queryRunner: QueryRunner; dryRun: boolean; operations: string[]; }): Promise<void> {
108
- const { field, model, entityMetadata, queryRunner, dryRun, operations } = params;
109
- const relationMetadata = entityMetadata?.relations.find((relation) => relation.propertyName === field.name);
110
- const resolvedTableName = entityMetadata?.tableName || model.tableName;
111
-
112
- if (field.type !== SolidFieldType.relation) {
113
- const columnCandidates = this.buildColumnCandidates(field, entityMetadata, relationMetadata);
114
- await this.dropColumnsForField(resolvedTableName, field, columnCandidates, queryRunner, dryRun, operations);
115
- return;
116
- }
117
-
118
- if (field.relationType === RelationType.manyToOne) {
119
- const columnCandidates = this.buildColumnCandidates(field, entityMetadata, relationMetadata);
120
- await this.dropColumnsForField(resolvedTableName, field, columnCandidates, queryRunner, dryRun, operations);
121
- return;
122
- }
123
-
124
- if (field.relationType === RelationType.manyTomany) {
125
- const joinTableName = relationMetadata?.junctionEntityMetadata?.tableName || field.relationJoinTableName;
126
- const ownsJoinTable = relationMetadata?.isOwning || field.isRelationManyToManyOwner;
127
-
128
- if (!ownsJoinTable) {
129
- operations.push(`No direct database cleanup required for inverse many-to-many field "${field.name}".`);
130
- return;
131
- }
132
-
133
- if (!joinTableName) {
134
- operations.push(`Skipping join-table cleanup for "${field.name}" because no join table name could be resolved.`);
135
- return;
136
- }
137
-
138
- await this.dropJoinTable(joinTableName, field, queryRunner, dryRun, operations);
139
- return;
140
- }
141
-
142
- operations.push(`No direct database cleanup required for relation field "${field.name}" with type "${field.relationType}".`);
143
- }
144
-
145
- private async dropColumnsForField(tableName: string, field: FieldMetadata, columnCandidates: string[], queryRunner: QueryRunner, dryRun: boolean, operations: string[],): Promise<void> {
146
- if (!tableName) {
147
- operations.push(`Skipping field "${field.name}" because the model table name could not be resolved.`);
148
- return;
149
- }
150
-
151
- if (columnCandidates.length === 0) {
152
- operations.push(`Skipping field "${field.name}" because no column candidates could be resolved.`);
153
- return;
154
- }
155
-
156
- const handledColumns = new Set<string>();
157
- let droppedAnyColumn = false;
158
-
159
- for (const columnName of columnCandidates) {
160
- if (!columnName || handledColumns.has(columnName)) {
161
- continue;
162
- }
163
-
164
- handledColumns.add(columnName);
165
- const table = await this.loadTable(queryRunner, tableName);
166
- const column = table?.columns.find((tableColumn) => tableColumn.name === columnName);
167
-
168
- if (!column) {
169
- continue;
170
- }
171
-
172
- droppedAnyColumn = true;
173
- await this.dropColumnArtifacts(table, columnName, queryRunner, dryRun, operations);
174
- }
175
-
176
- if (!droppedAnyColumn) {
177
- operations.push(`No database column found for field "${field.name}" on table "${tableName}". Metadata cleanup will still proceed.`);
178
- }
179
- }
180
-
181
- private async dropColumnArtifacts(table: Table, columnName: string, queryRunner: QueryRunner, dryRun: boolean, operations: string[],): Promise<void> {
182
- for (const foreignKey of table.foreignKeys.filter((item) => item.columnNames.includes(columnName))) {
183
- operations.push(`Drop foreign key "${foreignKey.name}" on "${table.name}.${columnName}"`);
184
- if (!dryRun) {
185
- await queryRunner.dropForeignKey(table, foreignKey);
186
- }
187
- }
188
-
189
- for (const uniqueConstraint of table.uniques.filter((item) => item.columnNames.includes(columnName))) {
190
- operations.push(`Drop unique constraint "${uniqueConstraint.name}" on "${table.name}.${columnName}"`);
191
- if (!dryRun) {
192
- await queryRunner.dropUniqueConstraint(table, uniqueConstraint);
193
- }
194
- }
195
-
196
- for (const index of table.indices.filter((item) => item.columnNames.includes(columnName))) {
197
- operations.push(`Drop index "${index.name}" on "${table.name}.${columnName}"`);
198
- if (!dryRun) {
199
- await queryRunner.dropIndex(table, index);
200
- }
201
- }
202
-
203
- operations.push(`Drop column "${table.name}.${columnName}"`);
204
- if (!dryRun) {
205
- await queryRunner.dropColumn(table, columnName);
206
- }
207
- }
208
-
209
- private async dropJoinTable(joinTableName: string, field: FieldMetadata, queryRunner: QueryRunner, dryRun: boolean, operations: string[]): Promise<void> {
210
- const table = await this.loadTable(queryRunner, joinTableName);
211
- if (!table) {
212
- operations.push(`Join table "${joinTableName}" for field "${field.name}" does not exist. Metadata cleanup will still proceed.`);
213
- return;
214
- }
215
-
216
- operations.push(`Drop join table "${joinTableName}" for field "${field.name}"`);
217
- if (!dryRun) {
218
- await queryRunner.dropTable(joinTableName);
219
- }
220
- }
221
-
222
- private buildColumnCandidates(field: FieldMetadata, entityMetadata?: EntityMetadata, relationMetadata?: EntityMetadata["relations"][number]): string[] {
223
- const columnCandidates = new Set<string>();
224
-
225
- relationMetadata?.joinColumns?.forEach((column) => {
226
- if (column.databaseName) {
227
- columnCandidates.add(column.databaseName);
228
- }
229
- });
230
-
231
- entityMetadata?.columns
232
- .filter((column) => column.propertyName === field.name)
233
- .forEach((column) => {
234
- if (column.databaseName) {
235
- columnCandidates.add(column.databaseName);
236
- }
237
- });
238
-
239
- if (field.columnName) {
240
- columnCandidates.add(field.columnName);
241
- }
242
-
243
- if (field.relationCoModelColumnName) {
244
- columnCandidates.add(field.relationCoModelColumnName);
245
- }
246
-
247
- if (field.type === SolidFieldType.relation && field.relationType === RelationType.manyToOne) {
248
- columnCandidates.add(`${snakeCase(field.name)}_id`);
249
- } else {
250
- columnCandidates.add(snakeCase(field.name));
251
- }
252
-
253
- return [...columnCandidates].filter(Boolean);
254
- }
255
-
256
- private async resolveDataSource(dataSourceName?: string): Promise<DataSource> {
257
- const normalizedDataSourceName = dataSourceName && dataSourceName !== "default" ? dataSourceName : undefined;
258
- const token = normalizedDataSourceName ? getDataSourceToken(normalizedDataSourceName) : getDataSourceToken();
259
- let dataSource: DataSource | undefined;
260
-
261
- try {
262
- dataSource = this.moduleRef.get<DataSource>(token, { strict: false });
263
- } catch (error: any) {
264
- throw new NotFoundException(`Datasource "${normalizedDataSourceName ?? "default"}" could not be resolved: ${error?.message ?? error}`);
265
- }
266
-
267
- if (!dataSource) {
268
- throw new NotFoundException(`Datasource "${normalizedDataSourceName ?? "default"}" could not be resolved.`);
269
- }
270
-
271
- if (!dataSource.isInitialized) {
272
- await dataSource.initialize();
273
- }
274
-
275
- return dataSource;
276
- }
277
-
278
- private resolveEntityMetadata(dataSource: DataSource, model: ModelMetadata): EntityMetadata | undefined {
279
- const candidates = [classify(model.singularName), model.singularName, model.tableName].filter(Boolean);
280
-
281
- for (const candidate of candidates) {
282
- try {
283
- return dataSource.getMetadata(candidate);
284
- } catch {
285
- // Try the next candidate.
286
- }
287
- }
288
-
289
- return dataSource.entityMetadatas.find((metadata) => metadata.tableName === model.tableName);
290
- }
291
-
292
- private async resolveMetadataFilePath(moduleName: string): Promise<string> {
293
- const defaultPath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName);
294
- if (await this.fileExists(defaultPath)) {
295
- return defaultPath;
296
- }
297
-
298
- const dashModuleName = kebabCase(moduleName);
299
- const moduleMetadataFilePath = path.resolve(
300
- process.cwd(),
301
- "module-metadata",
302
- dashModuleName,
303
- `${dashModuleName}-metadata.json`,
304
- );
305
-
306
- if (await this.fileExists(moduleMetadataFilePath)) {
307
- return moduleMetadataFilePath;
308
- }
309
-
310
- return defaultPath;
311
- }
312
-
313
- private async fileExists(filePath: string): Promise<boolean> {
314
- try {
315
- await fs.access(filePath);
316
- return true;
317
- } catch {
318
- return false;
319
- }
320
- }
321
-
322
- private async loadTable(queryRunner: QueryRunner, tableName: string): Promise<Table | undefined> {
323
- if (!tableName) {
324
- return undefined;
325
- }
326
-
327
- const hasTable = await queryRunner.hasTable(tableName);
328
- if (!hasTable) {
329
- return undefined;
330
- }
331
-
332
- return queryRunner.getTable(tableName) ?? undefined;
333
- }
334
- }