@solidxai/core 0.1.11-beta.0 → 0.1.11-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/CLAUDE.md +26 -0
- package/dist/commands/migrate-removed-fields.command.d.ts +19 -0
- package/dist/commands/migrate-removed-fields.command.d.ts.map +1 -0
- package/dist/commands/migrate-removed-fields.command.js +77 -0
- package/dist/commands/migrate-removed-fields.command.js.map +1 -0
- package/dist/controllers/datasource-introspection.controller.d.ts +46 -1
- package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -1
- package/dist/controllers/datasource-introspection.controller.js +38 -0
- package/dist/controllers/datasource-introspection.controller.js.map +1 -1
- package/dist/controllers/service.controller.d.ts +1 -0
- package/dist/controllers/service.controller.d.ts.map +1 -1
- package/dist/controllers/service.controller.js +2 -2
- package/dist/controllers/service.controller.js.map +1 -1
- package/dist/dtos/datasource-introspection-mapping.dto.d.ts +5 -0
- package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -1
- package/dist/dtos/datasource-introspection-mapping.dto.js +24 -2
- package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -1
- package/dist/entities/{legacy-common-with-id.entity.d.ts → legacy-common-with-generated-id.entity.d.ts} +2 -2
- package/dist/entities/legacy-common-with-generated-id.entity.d.ts.map +1 -0
- package/dist/entities/{legacy-common-with-id.entity.js → legacy-common-with-generated-id.entity.js} +4 -4
- package/dist/entities/legacy-common-with-generated-id.entity.js.map +1 -0
- package/dist/entities/{legacy-common.entity.d.ts → legacy-common.entity-with-existing-id.entity.d.ts} +1 -1
- package/dist/entities/legacy-common.entity-with-existing-id.entity.d.ts.map +1 -0
- package/dist/entities/{legacy-common.entity.js → legacy-common.entity-with-existing-id.entity.js} +1 -1
- package/dist/entities/legacy-common.entity-with-existing-id.entity.js.map +1 -0
- package/dist/helpers/command.service.d.ts +2 -0
- package/dist/helpers/command.service.d.ts.map +1 -1
- package/dist/helpers/command.service.js +6 -2
- package/dist/helpers/command.service.js.map +1 -1
- package/dist/helpers/environment.helper.d.ts +1 -0
- package/dist/helpers/environment.helper.d.ts.map +1 -1
- package/dist/helpers/environment.helper.js +4 -0
- package/dist/helpers/environment.helper.js.map +1 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.js +4 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.js.map +1 -1
- package/dist/helpers/model-metadata-helper.service.d.ts +2 -1
- package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
- package/dist/helpers/model-metadata-helper.service.js +9 -5
- package/dist/helpers/model-metadata-helper.service.js.map +1 -1
- package/dist/helpers/nodemon-heartbeat.d.ts +5 -0
- package/dist/helpers/nodemon-heartbeat.d.ts.map +1 -0
- package/dist/helpers/nodemon-heartbeat.js +67 -0
- package/dist/helpers/nodemon-heartbeat.js.map +1 -0
- package/dist/helpers/solid-registry.d.ts +2 -1
- package/dist/helpers/solid-registry.d.ts.map +1 -1
- package/dist/helpers/solid-registry.js.map +1 -1
- package/dist/helpers/typeorm-db-helper.d.ts +1 -0
- package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
- package/dist/helpers/typeorm-db-helper.js +12 -0
- package/dist/helpers/typeorm-db-helper.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/interfaces/datasource-introspection-provider.interface.d.ts +1 -0
- package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -1
- package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -1
- package/dist/interfaces.d.ts +1 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/repository/scheduled-job.repository.d.ts +5 -2
- package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
- package/dist/repository/scheduled-job.repository.js +66 -3
- package/dist/repository/scheduled-job.repository.js.map +1 -1
- package/dist/repository/security-rule.repository.d.ts +2 -2
- package/dist/repository/security-rule.repository.d.ts.map +1 -1
- package/dist/repository/security-rule.repository.js +6 -3
- package/dist/repository/security-rule.repository.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts +43 -2
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +1173 -232
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +128 -34
- 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/api-key.service.d.ts.map +1 -1
- package/dist/services/api-key.service.js.map +1 -1
- package/dist/services/authentication.service.d.ts +2 -0
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +32 -11
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +7 -5
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +4 -0
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -1
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +18 -1
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -1
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +25 -12
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/crud.service.d.ts +1 -0
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +27 -0
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +12 -0
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -1
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +2 -0
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -1
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +28 -15
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -1
- package/dist/services/datasource-introspection.service.d.ts +62 -2
- package/dist/services/datasource-introspection.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection.service.js +654 -186
- package/dist/services/datasource-introspection.service.js.map +1 -1
- package/dist/services/import-transaction.service.d.ts.map +1 -1
- package/dist/services/import-transaction.service.js +3 -0
- package/dist/services/import-transaction.service.js.map +1 -1
- package/dist/services/locale.service.d.ts +14 -0
- package/dist/services/locale.service.d.ts.map +1 -1
- package/dist/services/locale.service.js +83 -0
- package/dist/services/locale.service.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +10 -10
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
- package/dist/services/menu-item-metadata.service.d.ts.map +1 -1
- package/dist/services/menu-item-metadata.service.js +1 -0
- package/dist/services/menu-item-metadata.service.js.map +1 -1
- package/dist/services/model-metadata.service.d.ts +3 -0
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +78 -0
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/module-metadata.service.d.ts.map +1 -1
- package/dist/services/module-metadata.service.js +28 -0
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/removed-field-migration.service.d.ts +30 -0
- package/dist/services/removed-field-migration.service.d.ts.map +1 -0
- package/dist/services/removed-field-migration.service.js +319 -0
- package/dist/services/removed-field-migration.service.js.map +1 -0
- 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 +95 -33
- package/dist/services/role-metadata.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +244 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +120 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/solid-ts-morph.service.d.ts +6 -0
- package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
- package/dist/services/solid-ts-morph.service.js +43 -0
- 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 +70 -8
- 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 +4 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/audit.subscriber.d.ts +5 -1
- package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
- package/dist/subscribers/audit.subscriber.js +76 -12
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts +9 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.js +45 -11
- package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
- package/dist/subscribers/scheduled-job.subscriber.d.ts.map +1 -1
- package/dist/subscribers/scheduled-job.subscriber.js +3 -2
- package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
- package/dist/subscribers/security-rule.subscriber.d.ts.map +1 -1
- package/dist/subscribers/security-rule.subscriber.js +5 -3
- package/dist/subscribers/security-rule.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/migrate-removed-fields.command.ts +81 -0
- package/src/controllers/datasource-introspection.controller.ts +27 -1
- package/src/controllers/service.controller.ts +3 -2
- package/src/dtos/datasource-introspection-mapping.dto.ts +15 -0
- package/src/entities/{legacy-common-with-id.entity.ts → legacy-common-with-generated-id.entity.ts} +1 -1
- package/src/helpers/command.service.ts +10 -2
- package/src/helpers/environment.helper.ts +11 -0
- package/src/helpers/field-crud-managers/IntFieldCrudManager.ts +4 -1
- package/src/helpers/model-metadata-helper.service.ts +8 -3
- package/src/helpers/nodemon-heartbeat.ts +59 -0
- package/src/helpers/solid-registry.ts +7 -1
- package/src/helpers/typeorm-db-helper.ts +15 -0
- package/src/index.ts +3 -2
- package/src/interfaces/datasource-introspection-provider.interface.ts +1 -0
- package/src/interfaces.ts +1 -0
- package/src/repository/scheduled-job.repository.ts +83 -4
- package/src/repository/security-rule.repository.ts +10 -4
- package/src/seeders/module-metadata-seeder.service.ts +1415 -264
- package/src/seeders/seed-data/solid-core-metadata.json +128 -34
- package/src/services/action-metadata.service.ts +87 -2
- package/src/services/api-key.service.ts +36 -36
- package/src/services/authentication.service.ts +46 -12
- package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +11 -5
- package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +15 -1
- package/src/services/crud-helper.service.ts +25 -12
- package/src/services/crud.service.ts +36 -11
- package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +12 -0
- package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +2 -0
- package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +28 -15
- package/src/services/datasource-introspection.service.ts +811 -161
- package/src/services/import-transaction.service.ts +5 -0
- package/src/services/locale.service.ts +111 -3
- package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +2 -2
- package/src/services/menu-item-metadata.service.ts +2 -1
- package/src/services/model-metadata.service.ts +109 -1
- package/src/services/module-metadata.service.ts +36 -0
- package/src/services/removed-field-migration.service.ts +334 -0
- package/src/services/role-metadata.service.ts +112 -49
- package/src/services/settings/default-settings-provider.service.ts +120 -0
- package/src/services/solid-ts-morph.service.ts +61 -1
- package/src/services/view-metadata.service.ts +83 -9
- package/src/solid-core.module.ts +4 -0
- package/src/subscribers/audit.subscriber.ts +101 -18
- package/src/subscribers/computed-entity-field.subscriber.ts +81 -28
- package/src/subscribers/created-by-updated-by.subscriber.ts +3 -0
- package/src/subscribers/field-metadata.subscriber.ts +3 -0
- package/src/subscribers/scheduled-job.subscriber.ts +3 -2
- package/src/subscribers/security-rule.subscriber.ts +8 -3
- package/dist/entities/legacy-common-with-id.entity.d.ts.map +0 -1
- package/dist/entities/legacy-common-with-id.entity.js.map +0 -1
- package/dist/entities/legacy-common.entity.d.ts.map +0 -1
- package/dist/entities/legacy-common.entity.js.map +0 -1
- /package/src/entities/{legacy-common.entity.ts → legacy-common.entity-with-existing-id.entity.ts} +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
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
|
+
}
|
|
@@ -8,12 +8,14 @@ import { ERROR_MESSAGES } from 'src/constants/error-messages';
|
|
|
8
8
|
import { PermissionMetadataRepository } from 'src/repository/permission-metadata.repository';
|
|
9
9
|
import { RoleMetadataRepository } from 'src/repository/role-metadata.repository';
|
|
10
10
|
import { CreateRoleMetadataDto } from '../dtos/create-role-metadata.dto';
|
|
11
|
+
import { ModuleMetadata } from '../entities/module-metadata.entity';
|
|
11
12
|
import { PermissionMetadata } from '../entities/permission-metadata.entity';
|
|
12
13
|
import { RoleMetadata } from '../entities/role-metadata.entity';
|
|
13
14
|
|
|
14
15
|
@Injectable()
|
|
15
16
|
export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
16
17
|
private readonly logger = new Logger(RoleMetadataService.name);
|
|
18
|
+
private readonly permissionAssignmentBatchSize = 500;
|
|
17
19
|
|
|
18
20
|
constructor(
|
|
19
21
|
@InjectEntityManager()
|
|
@@ -64,13 +66,16 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
64
66
|
for (let id = 0; id < roles.length; id++) {
|
|
65
67
|
try {
|
|
66
68
|
const roleObj = roles[id];
|
|
69
|
+
const module = await this.resolveRoleModule(roleObj);
|
|
67
70
|
// this.logger.log(`Resolving role: ${JSON.stringify(roleObj)}`);
|
|
68
71
|
|
|
69
72
|
const existingRole = await this.repo.findOne({
|
|
70
73
|
where: {
|
|
71
74
|
name: roleObj.name,
|
|
72
75
|
},
|
|
73
|
-
relations: {
|
|
76
|
+
relations: {
|
|
77
|
+
module: true,
|
|
78
|
+
},
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
// Create only if not existing already.
|
|
@@ -86,9 +91,13 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
// const role = this.repo.create({ ...roleObj, permissions });
|
|
89
|
-
const role = this.repo.create({ ...roleObj });
|
|
94
|
+
const role = this.repo.create({ ...roleObj, ...(module ? { module } : {}) });
|
|
90
95
|
await this.repo.save(role);
|
|
91
96
|
} else {
|
|
97
|
+
if (module && existingRole.module?.id !== module.id) {
|
|
98
|
+
existingRole.module = module;
|
|
99
|
+
await this.repo.save(existingRole);
|
|
100
|
+
}
|
|
92
101
|
/*
|
|
93
102
|
this.logger.debug(`Role ${roleObj.name} already exists`);
|
|
94
103
|
const existingPermissions = existingRole.permissions.map(permission => permission.name);
|
|
@@ -111,6 +120,22 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
private async resolveRoleModule(roleObj: CreateRoleMetadataDto): Promise<ModuleMetadata | null> {
|
|
124
|
+
if (roleObj.moduleId) {
|
|
125
|
+
return await this.entityManager.getRepository(ModuleMetadata).findOne({
|
|
126
|
+
where: { id: roleObj.moduleId },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (roleObj.moduleUserKey) {
|
|
131
|
+
return await this.entityManager.getRepository(ModuleMetadata).findOne({
|
|
132
|
+
where: { name: roleObj.moduleUserKey },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
async addAllPermissionsToRole(roleName: string): Promise<RoleMetadata> {
|
|
115
140
|
return await this._addPermissionsToRole(roleName, null);
|
|
116
141
|
}
|
|
@@ -126,66 +151,47 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
126
151
|
private async _addPermissionsToRole(roleName: string, permissionNames: string[]): Promise<RoleMetadata> {
|
|
127
152
|
const role = await this.repo.findOne({
|
|
128
153
|
where: { name: roleName },
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
select: {
|
|
155
|
+
id: true,
|
|
156
|
+
name: true,
|
|
131
157
|
}
|
|
132
158
|
});
|
|
133
159
|
if (!role) {
|
|
134
160
|
throw new Error(`Role '${roleName}' not found.`);
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
|
|
163
|
+
const joinInfo = this.getRolePermissionJoinInfo();
|
|
164
|
+
const targetPermissions = await this.loadTargetPermissions(permissionNames);
|
|
138
165
|
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
if (!permissionNames || permissionNames.length === 0) {
|
|
167
|
+
await this.entityManager
|
|
168
|
+
.createQueryBuilder()
|
|
169
|
+
.delete()
|
|
170
|
+
.from(joinInfo.tableName)
|
|
171
|
+
.where(`${joinInfo.roleIdColumn} = :roleId`, { roleId: role.id })
|
|
172
|
+
.execute();
|
|
141
173
|
|
|
142
|
-
|
|
143
|
-
if (permissionNames && permissionNames.length != 0) {
|
|
144
|
-
// this.logger.log(`Loading specified permissions.`);
|
|
145
|
-
|
|
146
|
-
newPermissions = await this.permissionRepository.find({ where: { name: In(permissionNames) } });
|
|
147
|
-
if (newPermissions.length !== permissionNames.length) {
|
|
148
|
-
throw new Error(`One or more permissions not found.`);
|
|
149
|
-
}
|
|
174
|
+
await this.insertRolePermissionMappings(role.id, targetPermissions.map((permission) => permission.id), joinInfo);
|
|
150
175
|
}
|
|
151
176
|
else {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (let i = 0; i < newPermissions.length; i++) {
|
|
167
|
-
const newPermission = newPermissions[i];
|
|
168
|
-
let newPermissionFound = true;
|
|
169
|
-
for (let j = 0; j < role.permissions.length; j++) {
|
|
170
|
-
const existingPermission = role.permissions[j];
|
|
171
|
-
if (existingPermission.name === newPermission.name) {
|
|
172
|
-
newPermissionFound = false;
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (newPermissionFound) {
|
|
178
|
-
role.permissions.push(newPermission);
|
|
179
|
-
}
|
|
177
|
+
const existingRows = await this.entityManager
|
|
178
|
+
.createQueryBuilder()
|
|
179
|
+
.select(joinInfo.permissionIdColumn, 'permissionId')
|
|
180
|
+
.from(joinInfo.tableName, 'role_permission')
|
|
181
|
+
.where(`${joinInfo.roleIdColumn} = :roleId`, { roleId: role.id })
|
|
182
|
+
.getRawMany();
|
|
183
|
+
|
|
184
|
+
const existingPermissionIds = new Set(existingRows.map((row) => Number(row.permissionId)));
|
|
185
|
+
const missingPermissionIds = targetPermissions
|
|
186
|
+
.map((permission) => permission.id)
|
|
187
|
+
.filter((permissionId) => !existingPermissionIds.has(permissionId));
|
|
188
|
+
|
|
189
|
+
if (missingPermissionIds.length > 0) {
|
|
190
|
+
await this.insertRolePermissionMappings(role.id, missingPermissionIds, joinInfo);
|
|
180
191
|
}
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
// else we create a new permissions set.
|
|
184
|
-
else {
|
|
185
|
-
role.permissions = newPermissions;
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
return await this.
|
|
194
|
+
return await this.findRoleByName(roleName);
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
async removePermissionsFromRole(roleName: string, permissionNames: string[]): Promise<RoleMetadata> {
|
|
@@ -220,5 +226,62 @@ export class RoleMetadataService extends CRUDService<RoleMetadata> {
|
|
|
220
226
|
return existingPermission;
|
|
221
227
|
}
|
|
222
228
|
|
|
229
|
+
private async loadTargetPermissions(permissionNames: string[] | null): Promise<PermissionMetadata[]> {
|
|
230
|
+
if (permissionNames && permissionNames.length !== 0) {
|
|
231
|
+
const uniquePermissionNames = [...new Set(permissionNames)];
|
|
232
|
+
const permissions = await this.permissionRepository.find({ where: { name: In(uniquePermissionNames) } });
|
|
233
|
+
if (permissions.length !== uniquePermissionNames.length) {
|
|
234
|
+
throw new Error(`One or more permissions not found.`);
|
|
235
|
+
}
|
|
236
|
+
return permissions;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const permissions = await this.permissionRepository.find();
|
|
240
|
+
if (permissions.length === 0) {
|
|
241
|
+
throw new Error(`No permissions configured in the system. Did you forget to run the PermissionSeederService?`);
|
|
242
|
+
}
|
|
243
|
+
return permissions;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private getRolePermissionJoinInfo(): { tableName: string; roleIdColumn: string; permissionIdColumn: string } {
|
|
247
|
+
const relation = this.repo.metadata.relations.find((candidate) => candidate.propertyName === 'permissions');
|
|
248
|
+
if (!relation?.junctionEntityMetadata) {
|
|
249
|
+
throw new Error('Role-permission join table metadata not found.');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const roleIdColumn = relation.joinColumns[0]?.databaseName;
|
|
253
|
+
const permissionIdColumn = relation.inverseJoinColumns[0]?.databaseName;
|
|
254
|
+
if (!roleIdColumn || !permissionIdColumn) {
|
|
255
|
+
throw new Error('Role-permission join table column metadata not found.');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
tableName: relation.junctionEntityMetadata.tableName,
|
|
260
|
+
roleIdColumn,
|
|
261
|
+
permissionIdColumn,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async insertRolePermissionMappings(
|
|
266
|
+
roleId: number,
|
|
267
|
+
permissionIds: number[],
|
|
268
|
+
joinInfo: { tableName: string; roleIdColumn: string; permissionIdColumn: string },
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
for (let index = 0; index < permissionIds.length; index += this.permissionAssignmentBatchSize) {
|
|
271
|
+
const batch = permissionIds.slice(index, index + this.permissionAssignmentBatchSize);
|
|
272
|
+
await this.entityManager
|
|
273
|
+
.createQueryBuilder()
|
|
274
|
+
.insert()
|
|
275
|
+
.into(joinInfo.tableName)
|
|
276
|
+
.values(
|
|
277
|
+
batch.map((permissionId) => ({
|
|
278
|
+
[joinInfo.roleIdColumn]: roleId,
|
|
279
|
+
[joinInfo.permissionIdColumn]: permissionId,
|
|
280
|
+
})),
|
|
281
|
+
)
|
|
282
|
+
.execute();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
223
286
|
|
|
224
287
|
}
|