@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.
Files changed (226) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/CLAUDE.md +26 -0
  3. package/dist/commands/migrate-removed-fields.command.d.ts +19 -0
  4. package/dist/commands/migrate-removed-fields.command.d.ts.map +1 -0
  5. package/dist/commands/migrate-removed-fields.command.js +77 -0
  6. package/dist/commands/migrate-removed-fields.command.js.map +1 -0
  7. package/dist/controllers/datasource-introspection.controller.d.ts +46 -1
  8. package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -1
  9. package/dist/controllers/datasource-introspection.controller.js +38 -0
  10. package/dist/controllers/datasource-introspection.controller.js.map +1 -1
  11. package/dist/controllers/service.controller.d.ts +1 -0
  12. package/dist/controllers/service.controller.d.ts.map +1 -1
  13. package/dist/controllers/service.controller.js +2 -2
  14. package/dist/controllers/service.controller.js.map +1 -1
  15. package/dist/dtos/datasource-introspection-mapping.dto.d.ts +5 -0
  16. package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -1
  17. package/dist/dtos/datasource-introspection-mapping.dto.js +24 -2
  18. package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -1
  19. package/dist/entities/{legacy-common-with-id.entity.d.ts → legacy-common-with-generated-id.entity.d.ts} +2 -2
  20. package/dist/entities/legacy-common-with-generated-id.entity.d.ts.map +1 -0
  21. package/dist/entities/{legacy-common-with-id.entity.js → legacy-common-with-generated-id.entity.js} +4 -4
  22. package/dist/entities/legacy-common-with-generated-id.entity.js.map +1 -0
  23. package/dist/entities/{legacy-common.entity.d.ts → legacy-common.entity-with-existing-id.entity.d.ts} +1 -1
  24. package/dist/entities/legacy-common.entity-with-existing-id.entity.d.ts.map +1 -0
  25. package/dist/entities/{legacy-common.entity.js → legacy-common.entity-with-existing-id.entity.js} +1 -1
  26. package/dist/entities/legacy-common.entity-with-existing-id.entity.js.map +1 -0
  27. package/dist/helpers/command.service.d.ts +2 -0
  28. package/dist/helpers/command.service.d.ts.map +1 -1
  29. package/dist/helpers/command.service.js +6 -2
  30. package/dist/helpers/command.service.js.map +1 -1
  31. package/dist/helpers/environment.helper.d.ts +1 -0
  32. package/dist/helpers/environment.helper.d.ts.map +1 -1
  33. package/dist/helpers/environment.helper.js +4 -0
  34. package/dist/helpers/environment.helper.js.map +1 -1
  35. package/dist/helpers/field-crud-managers/IntFieldCrudManager.d.ts.map +1 -1
  36. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js +4 -1
  37. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js.map +1 -1
  38. package/dist/helpers/model-metadata-helper.service.d.ts +2 -1
  39. package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
  40. package/dist/helpers/model-metadata-helper.service.js +9 -5
  41. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  42. package/dist/helpers/nodemon-heartbeat.d.ts +5 -0
  43. package/dist/helpers/nodemon-heartbeat.d.ts.map +1 -0
  44. package/dist/helpers/nodemon-heartbeat.js +67 -0
  45. package/dist/helpers/nodemon-heartbeat.js.map +1 -0
  46. package/dist/helpers/solid-registry.d.ts +2 -1
  47. package/dist/helpers/solid-registry.d.ts.map +1 -1
  48. package/dist/helpers/solid-registry.js.map +1 -1
  49. package/dist/helpers/typeorm-db-helper.d.ts +1 -0
  50. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  51. package/dist/helpers/typeorm-db-helper.js +12 -0
  52. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  53. package/dist/index.d.ts +3 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/interfaces/datasource-introspection-provider.interface.d.ts +1 -0
  58. package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -1
  59. package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -1
  60. package/dist/interfaces.d.ts +1 -0
  61. package/dist/interfaces.d.ts.map +1 -1
  62. package/dist/interfaces.js.map +1 -1
  63. package/dist/repository/scheduled-job.repository.d.ts +5 -2
  64. package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
  65. package/dist/repository/scheduled-job.repository.js +66 -3
  66. package/dist/repository/scheduled-job.repository.js.map +1 -1
  67. package/dist/repository/security-rule.repository.d.ts +2 -2
  68. package/dist/repository/security-rule.repository.d.ts.map +1 -1
  69. package/dist/repository/security-rule.repository.js +6 -3
  70. package/dist/repository/security-rule.repository.js.map +1 -1
  71. package/dist/seeders/module-metadata-seeder.service.d.ts +43 -2
  72. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  73. package/dist/seeders/module-metadata-seeder.service.js +1173 -232
  74. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  75. package/dist/seeders/seed-data/solid-core-metadata.json +128 -34
  76. package/dist/services/action-metadata.service.d.ts +7 -0
  77. package/dist/services/action-metadata.service.d.ts.map +1 -1
  78. package/dist/services/action-metadata.service.js +76 -3
  79. package/dist/services/action-metadata.service.js.map +1 -1
  80. package/dist/services/api-key.service.d.ts.map +1 -1
  81. package/dist/services/api-key.service.js.map +1 -1
  82. package/dist/services/authentication.service.d.ts +2 -0
  83. package/dist/services/authentication.service.d.ts.map +1 -1
  84. package/dist/services/authentication.service.js +32 -11
  85. package/dist/services/authentication.service.js.map +1 -1
  86. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
  87. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +7 -5
  88. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
  89. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +4 -0
  90. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -1
  91. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +18 -1
  92. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -1
  93. package/dist/services/crud-helper.service.d.ts.map +1 -1
  94. package/dist/services/crud-helper.service.js +25 -12
  95. package/dist/services/crud-helper.service.js.map +1 -1
  96. package/dist/services/crud.service.d.ts +1 -0
  97. package/dist/services/crud.service.d.ts.map +1 -1
  98. package/dist/services/crud.service.js +27 -0
  99. package/dist/services/crud.service.js.map +1 -1
  100. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -1
  101. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +12 -0
  102. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -1
  103. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -1
  104. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +2 -0
  105. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -1
  106. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -1
  107. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +28 -15
  108. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -1
  109. package/dist/services/datasource-introspection.service.d.ts +62 -2
  110. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  111. package/dist/services/datasource-introspection.service.js +654 -186
  112. package/dist/services/datasource-introspection.service.js.map +1 -1
  113. package/dist/services/import-transaction.service.d.ts.map +1 -1
  114. package/dist/services/import-transaction.service.js +3 -0
  115. package/dist/services/import-transaction.service.js.map +1 -1
  116. package/dist/services/locale.service.d.ts +14 -0
  117. package/dist/services/locale.service.d.ts.map +1 -1
  118. package/dist/services/locale.service.js +83 -0
  119. package/dist/services/locale.service.js.map +1 -1
  120. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +10 -10
  121. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  122. package/dist/services/menu-item-metadata.service.d.ts.map +1 -1
  123. package/dist/services/menu-item-metadata.service.js +1 -0
  124. package/dist/services/menu-item-metadata.service.js.map +1 -1
  125. package/dist/services/model-metadata.service.d.ts +3 -0
  126. package/dist/services/model-metadata.service.d.ts.map +1 -1
  127. package/dist/services/model-metadata.service.js +78 -0
  128. package/dist/services/model-metadata.service.js.map +1 -1
  129. package/dist/services/module-metadata.service.d.ts.map +1 -1
  130. package/dist/services/module-metadata.service.js +28 -0
  131. package/dist/services/module-metadata.service.js.map +1 -1
  132. package/dist/services/removed-field-migration.service.d.ts +30 -0
  133. package/dist/services/removed-field-migration.service.d.ts.map +1 -0
  134. package/dist/services/removed-field-migration.service.js +319 -0
  135. package/dist/services/removed-field-migration.service.js.map +1 -0
  136. package/dist/services/role-metadata.service.d.ts +5 -0
  137. package/dist/services/role-metadata.service.d.ts.map +1 -1
  138. package/dist/services/role-metadata.service.js +95 -33
  139. package/dist/services/role-metadata.service.js.map +1 -1
  140. package/dist/services/settings/default-settings-provider.service.d.ts +244 -0
  141. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  142. package/dist/services/settings/default-settings-provider.service.js +120 -0
  143. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  144. package/dist/services/solid-ts-morph.service.d.ts +6 -0
  145. package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
  146. package/dist/services/solid-ts-morph.service.js +43 -0
  147. package/dist/services/solid-ts-morph.service.js.map +1 -1
  148. package/dist/services/view-metadata.service.d.ts +6 -0
  149. package/dist/services/view-metadata.service.d.ts.map +1 -1
  150. package/dist/services/view-metadata.service.js +70 -8
  151. package/dist/services/view-metadata.service.js.map +1 -1
  152. package/dist/solid-core.module.d.ts.map +1 -1
  153. package/dist/solid-core.module.js +4 -0
  154. package/dist/solid-core.module.js.map +1 -1
  155. package/dist/subscribers/audit.subscriber.d.ts +5 -1
  156. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  157. package/dist/subscribers/audit.subscriber.js +76 -12
  158. package/dist/subscribers/audit.subscriber.js.map +1 -1
  159. package/dist/subscribers/computed-entity-field.subscriber.d.ts +9 -1
  160. package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
  161. package/dist/subscribers/computed-entity-field.subscriber.js +45 -11
  162. package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
  163. package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
  164. package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
  165. package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
  166. package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
  167. package/dist/subscribers/scheduled-job.subscriber.d.ts.map +1 -1
  168. package/dist/subscribers/scheduled-job.subscriber.js +3 -2
  169. package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
  170. package/dist/subscribers/security-rule.subscriber.d.ts.map +1 -1
  171. package/dist/subscribers/security-rule.subscriber.js +5 -3
  172. package/dist/subscribers/security-rule.subscriber.js.map +1 -1
  173. package/package.json +1 -1
  174. package/src/commands/migrate-removed-fields.command.ts +81 -0
  175. package/src/controllers/datasource-introspection.controller.ts +27 -1
  176. package/src/controllers/service.controller.ts +3 -2
  177. package/src/dtos/datasource-introspection-mapping.dto.ts +15 -0
  178. package/src/entities/{legacy-common-with-id.entity.ts → legacy-common-with-generated-id.entity.ts} +1 -1
  179. package/src/helpers/command.service.ts +10 -2
  180. package/src/helpers/environment.helper.ts +11 -0
  181. package/src/helpers/field-crud-managers/IntFieldCrudManager.ts +4 -1
  182. package/src/helpers/model-metadata-helper.service.ts +8 -3
  183. package/src/helpers/nodemon-heartbeat.ts +59 -0
  184. package/src/helpers/solid-registry.ts +7 -1
  185. package/src/helpers/typeorm-db-helper.ts +15 -0
  186. package/src/index.ts +3 -2
  187. package/src/interfaces/datasource-introspection-provider.interface.ts +1 -0
  188. package/src/interfaces.ts +1 -0
  189. package/src/repository/scheduled-job.repository.ts +83 -4
  190. package/src/repository/security-rule.repository.ts +10 -4
  191. package/src/seeders/module-metadata-seeder.service.ts +1415 -264
  192. package/src/seeders/seed-data/solid-core-metadata.json +128 -34
  193. package/src/services/action-metadata.service.ts +87 -2
  194. package/src/services/api-key.service.ts +36 -36
  195. package/src/services/authentication.service.ts +46 -12
  196. package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +11 -5
  197. package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +15 -1
  198. package/src/services/crud-helper.service.ts +25 -12
  199. package/src/services/crud.service.ts +36 -11
  200. package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +12 -0
  201. package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +2 -0
  202. package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +28 -15
  203. package/src/services/datasource-introspection.service.ts +811 -161
  204. package/src/services/import-transaction.service.ts +5 -0
  205. package/src/services/locale.service.ts +111 -3
  206. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +2 -2
  207. package/src/services/menu-item-metadata.service.ts +2 -1
  208. package/src/services/model-metadata.service.ts +109 -1
  209. package/src/services/module-metadata.service.ts +36 -0
  210. package/src/services/removed-field-migration.service.ts +334 -0
  211. package/src/services/role-metadata.service.ts +112 -49
  212. package/src/services/settings/default-settings-provider.service.ts +120 -0
  213. package/src/services/solid-ts-morph.service.ts +61 -1
  214. package/src/services/view-metadata.service.ts +83 -9
  215. package/src/solid-core.module.ts +4 -0
  216. package/src/subscribers/audit.subscriber.ts +101 -18
  217. package/src/subscribers/computed-entity-field.subscriber.ts +81 -28
  218. package/src/subscribers/created-by-updated-by.subscriber.ts +3 -0
  219. package/src/subscribers/field-metadata.subscriber.ts +3 -0
  220. package/src/subscribers/scheduled-job.subscriber.ts +3 -2
  221. package/src/subscribers/security-rule.subscriber.ts +8 -3
  222. package/dist/entities/legacy-common-with-id.entity.d.ts.map +0 -1
  223. package/dist/entities/legacy-common-with-id.entity.js.map +0 -1
  224. package/dist/entities/legacy-common.entity.d.ts.map +0 -1
  225. package/dist/entities/legacy-common.entity.js.map +0 -1
  226. /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
- relations: {
130
- permissions: true
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
- // this.logger.log(`Found role ${roleName}`);
163
+ const joinInfo = this.getRolePermissionJoinInfo();
164
+ const targetPermissions = await this.loadTargetPermissions(permissionNames);
138
165
 
139
- // The new set of permissions which are to be added to this role.
140
- let newPermissions: PermissionMetadata[];
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
- // Load all the specified permissions in the system.
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
- // this.logger.log(`Loading all permissions in system.`);
153
-
154
- // Load all permissions in the system.
155
- // TODO: Do we want to convert this to a paginated query to avoid having to load a very large permissions table into memory?
156
- newPermissions = await this.permissionRepository.find();
157
- if (newPermissions.length == 0) {
158
- throw new Error(`No permissions configured in the system. Did you forget to run the PermissionSeederService?`);
159
- }
160
- }
161
-
162
- // this.logger.log(`Adding ${newPermissions.length} permissions to role ${roleName}.`);
163
-
164
- // if there are already permissions assigned.
165
- if (role.permissions && role.permissions.length > 0) {
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.repo.save(role);
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
  }