@solidxai/core 0.1.10 → 0.1.11-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/datasource-introspection.controller.d.ts +234 -0
- package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -0
- package/dist/controllers/datasource-introspection.controller.js +98 -0
- package/dist/controllers/datasource-introspection.controller.js.map +1 -0
- package/dist/controllers/datasource-management.controller.d.ts +18 -0
- package/dist/controllers/datasource-management.controller.d.ts.map +1 -0
- package/dist/controllers/datasource-management.controller.js +55 -0
- package/dist/controllers/datasource-management.controller.js.map +1 -0
- package/dist/dtos/create-datasource-management.dto.d.ts +25 -0
- package/dist/dtos/create-datasource-management.dto.d.ts.map +1 -0
- package/dist/dtos/create-datasource-management.dto.js +169 -0
- package/dist/dtos/create-datasource-management.dto.js.map +1 -0
- package/dist/dtos/datasource-introspection-mapping.dto.d.ts +45 -0
- package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -0
- package/dist/dtos/datasource-introspection-mapping.dto.js +233 -0
- package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/datasource-introspection-provider.interface.d.ts +26 -0
- package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -0
- package/dist/interfaces/datasource-introspection-provider.interface.js +3 -0
- package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -0
- package/dist/repository/scheduled-job.repository.d.ts +3 -0
- package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
- package/dist/repository/scheduled-job.repository.js +60 -0
- package/dist/repository/scheduled-job.repository.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts +32 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +1011 -230
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +21 -0
- package/dist/services/action-metadata.service.d.ts +7 -0
- package/dist/services/action-metadata.service.d.ts.map +1 -1
- package/dist/services/action-metadata.service.js +76 -3
- package/dist/services/action-metadata.service.js.map +1 -1
- package/dist/services/authentication.service.js +1 -1
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.d.ts.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.js +2 -2
- package/dist/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.js.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.d.ts.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.js +2 -2
- package/dist/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.js.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-provider-utils.d.ts +3 -0
- package/dist/services/dashboard-providers/mq-dashboard-provider-utils.d.ts.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-provider-utils.js +71 -0
- package/dist/services/dashboard-providers/mq-dashboard-provider-utils.js.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.d.ts.map +1 -1
- package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.js +3 -3
- package/dist/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.js.map +1 -1
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts +11 -0
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -0
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +181 -0
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -0
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts +11 -0
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -0
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +90 -0
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -0
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts +11 -0
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -0
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +156 -0
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -0
- package/dist/services/datasource-introspection.service.d.ts +288 -0
- package/dist/services/datasource-introspection.service.d.ts.map +1 -0
- package/dist/services/datasource-introspection.service.js +1278 -0
- package/dist/services/datasource-introspection.service.js.map +1 -0
- package/dist/services/datasource-management.service.d.ts +60 -0
- package/dist/services/datasource-management.service.d.ts.map +1 -0
- package/dist/services/datasource-management.service.js +461 -0
- package/dist/services/datasource-management.service.js.map +1 -0
- package/dist/services/field-metadata.service.d.ts.map +1 -1
- package/dist/services/field-metadata.service.js +19 -0
- package/dist/services/field-metadata.service.js.map +1 -1
- package/dist/services/model-metadata.service.d.ts +1 -3
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +47 -15
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/role-metadata.service.d.ts +5 -0
- package/dist/services/role-metadata.service.d.ts.map +1 -1
- package/dist/services/role-metadata.service.js +85 -31
- package/dist/services/role-metadata.service.js.map +1 -1
- package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.d.ts.map +1 -1
- package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.js +2 -3
- package/dist/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.js.map +1 -1
- package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.d.ts.map +1 -1
- package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.js +2 -3
- package/dist/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.js.map +1 -1
- package/dist/services/setting.service.d.ts +1 -0
- package/dist/services/setting.service.d.ts.map +1 -1
- package/dist/services/setting.service.js +32 -8
- package/dist/services/setting.service.js.map +1 -1
- package/dist/services/solid-ts-morph.service.d.ts +1 -1
- package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
- package/dist/services/solid-ts-morph.service.js.map +1 -1
- package/dist/services/view-metadata.service.d.ts +6 -0
- package/dist/services/view-metadata.service.d.ts.map +1 -1
- package/dist/services/view-metadata.service.js +66 -1
- package/dist/services/view-metadata.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +16 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist-tests/api/authenticate.spec.js +119 -0
- package/dist-tests/api/authenticate.spec.js.map +1 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
- package/dist-tests/api/ping.spec.js +21 -0
- package/dist-tests/api/ping.spec.js.map +1 -0
- package/dist-tests/helpers/auth.js +41 -0
- package/dist-tests/helpers/auth.js.map +1 -0
- package/dist-tests/helpers/env.js +11 -0
- package/dist-tests/helpers/env.js.map +1 -0
- package/docs/agent-hub-grooming.md +301 -0
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
- package/docs/datasource-introspection-ddl-analysis.md +326 -0
- package/docs/datasource-introspection-implementation-plan.md +306 -0
- package/docs/grouping-enhancements.md +89 -0
- package/docs/java-spring/README.md +3 -0
- package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
- package/docs/module-package-import-handoff.md +691 -0
- package/docs/seed-changes.md +65 -0
- package/docs/test-data-workflow.md +200 -0
- package/docs/type-declaration-import-issue.md +24 -0
- package/package.json +1 -1
- package/src/controllers/datasource-introspection.controller.ts +58 -0
- package/src/controllers/datasource-management.controller.ts +24 -0
- package/src/dtos/create-datasource-management.dto.ts +139 -0
- package/src/dtos/datasource-introspection-mapping.dto.ts +182 -0
- package/src/index.ts +5 -0
- package/src/interfaces/datasource-introspection-provider.interface.ts +28 -0
- package/src/repository/scheduled-job.repository.ts +73 -0
- package/src/seeders/module-metadata-seeder.service.ts +1223 -262
- package/src/seeders/seed-data/solid-core-metadata.json +22 -1
- package/src/services/action-metadata.service.ts +87 -2
- package/src/services/authentication.service.ts +1 -1
- package/src/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.ts +9 -4
- package/src/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.ts +9 -4
- package/src/services/dashboard-providers/mq-dashboard-provider-utils.ts +82 -0
- package/src/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.ts +10 -4
- package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +185 -0
- package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +95 -0
- package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +160 -0
- package/src/services/datasource-introspection.service.ts +1536 -0
- package/src/services/datasource-management.service.ts +566 -0
- package/src/services/field-metadata.service.ts +21 -1
- package/src/services/model-metadata.service.ts +50 -13
- package/src/services/role-metadata.service.ts +100 -47
- package/src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts +2 -4
- package/src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts +2 -4
- package/src/services/setting.service.ts +43 -12
- package/src/services/solid-ts-morph.service.ts +2 -2
- package/src/services/view-metadata.service.ts +79 -1
- package/src/solid-core.module.ts +16 -0
- package/.claude/settings.local.json +0 -15
- package/src/services/1.js +0 -6
|
@@ -14,7 +14,7 @@ import { ListOfValuesService } from 'src/services/list-of-values.service';
|
|
|
14
14
|
import { SettingService } from 'src/services/setting.service';
|
|
15
15
|
import { SmsTemplateService } from 'src/services/sms-template.service';
|
|
16
16
|
import { UserService } from 'src/services/user.service';
|
|
17
|
-
import { DataSource, In } from 'typeorm';
|
|
17
|
+
import { DataSource, In, Repository } from 'typeorm';
|
|
18
18
|
import { CreateModelMetadataDto } from '../dtos/create-model-metadata.dto';
|
|
19
19
|
import { CreateModuleMetadataDto } from '../dtos/create-module-metadata.dto';
|
|
20
20
|
import { getDynamicModuleNamesBasedOnMetadata } from '../helpers/module.helper';
|
|
@@ -55,10 +55,37 @@ import { ModelMetadata } from 'src/entities/model-metadata.entity';
|
|
|
55
55
|
import { PermissionMetadata } from 'src/entities/permission-metadata.entity';
|
|
56
56
|
import { ViewMetadata } from 'src/entities/view-metadata.entity';
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Central metadata seeder for both solid-core and consuming modules.
|
|
60
|
+
*
|
|
61
|
+
* Performance notes:
|
|
62
|
+
* - This file now contains the main seed-time optimizations that were added after profiling real FRMS seed runs.
|
|
63
|
+
* - The goal of those changes was to preserve existing behavior while removing avoidable serial database work.
|
|
64
|
+
* - Verbose timing logs are intentionally emitted through Nest logger `debug()` only, so normal CLI runs stay clean.
|
|
65
|
+
* - Every timing line is tagged with `[SEED_TIMING]` and a stable `[ITEM:...]` identifier for grep-friendly analysis.
|
|
66
|
+
*
|
|
67
|
+
* High-impact optimizations currently implemented here:
|
|
68
|
+
* - Batched permission preload/insert, with case-insensitive dedupe to match MSSQL collation behavior.
|
|
69
|
+
* - Process-local lookup caches for module/model/view/provider reads during a single seed run.
|
|
70
|
+
* - Model-level field preload/diff to avoid field-level serial existence checks.
|
|
71
|
+
* - View/action preload + in-memory no-op detection so unchanged rows skip save() entirely.
|
|
72
|
+
* - Menu entity no-op detection, though menu relation lookups are still mostly serial and remain a future candidate.
|
|
73
|
+
*/
|
|
59
74
|
@Injectable()
|
|
60
75
|
export class ModuleMetadataSeederService {
|
|
61
76
|
private readonly logger = new Logger(ModuleMetadataSeederService.name);
|
|
77
|
+
// Stable tag used on all verbose seed timing logs so runs can be grepped quickly.
|
|
78
|
+
private readonly seedTimingTag = 'SEED_TIMING';
|
|
79
|
+
// Keep permission batching bounded while still collapsing thousands of serial checks into a few queries.
|
|
80
|
+
private readonly permissionSeedBatchSize = 500;
|
|
81
|
+
// Restrict no-op detection to the fields that actually matter for persistence.
|
|
82
|
+
private readonly viewComparableFields = ['name', 'displayName', 'type', 'context', 'layout', 'module', 'model'] as const;
|
|
83
|
+
private readonly actionComparableFields = ['name', 'displayName', 'type', 'domain', 'context', 'customComponent', 'customIsModal', 'serverEndpoint', 'module', 'model', 'view'] as const;
|
|
84
|
+
// These caches are intentionally process-local and are reset at the beginning of every seed run.
|
|
85
|
+
private readonly moduleLookupCache = new Map<string, ModuleMetadata | null>();
|
|
86
|
+
private readonly modelLookupCache = new Map<string, ModelMetadata | null>();
|
|
87
|
+
private readonly viewLookupCache = new Map<string, ViewMetadata | null>();
|
|
88
|
+
private readonly mediaStorageProviderLookupCache = new Map<string, any | null>();
|
|
62
89
|
private enablePruning: boolean = false;
|
|
63
90
|
|
|
64
91
|
constructor(
|
|
@@ -99,139 +126,145 @@ export class ModuleMetadataSeederService {
|
|
|
99
126
|
|
|
100
127
|
try {
|
|
101
128
|
this.enablePruning = Boolean(conf?.pruneMetadata);
|
|
129
|
+
// Never reuse cached entity snapshots across seed invocations in the same process.
|
|
130
|
+
this.resetLookupCaches();
|
|
102
131
|
console.log(this.enablePruning ? '▶ Pruning enabled: metadata not present in JSON will be removed.' : '▶ Pruning disabled: existing metadata will be kept.');
|
|
103
132
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Module specific seeding steps.
|
|
113
|
-
// Get all the module metadata files which needs to be seeded.
|
|
114
|
-
const seedDataFiles = this.seedDataFiles;
|
|
115
|
-
this.logger.debug(`Found seed data for modules: ${seedDataFiles.map(s => s.moduleMetadata?.name)}`);
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* -------------------------------------------------------------
|
|
119
|
-
* Selective module seeding via: solid seed --modules-to-seed onboarding,reports
|
|
120
|
-
* -------------------------------------------------------------
|
|
121
|
-
*/
|
|
122
|
-
currentStep = 'resolveModulesToSeed';
|
|
123
|
-
if (conf && Array.isArray(conf.modulesToSeed)) {
|
|
124
|
-
modulesToSeed = conf.modulesToSeed;
|
|
125
|
-
console.log(`▶ Selective seeding enabled. Modules to seed: ${modulesToSeed.join(', ')}`);
|
|
126
|
-
this.logger.log(`Selective seeding enabled. Modules to seed: ${modulesToSeed.join(', ')}`);
|
|
127
|
-
} else {
|
|
128
|
-
console.log(`▶ No modulesToSeed provided. Seeding ALL modules.`);
|
|
129
|
-
this.logger.log(`No modulesToSeed provided. Seeding ALL modules.`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Filter modules if needed
|
|
133
|
-
const filteredSeedDataFiles = modulesToSeed ? seedDataFiles.filter((file) => modulesToSeed.includes(file.moduleMetadata?.name)) : seedDataFiles;
|
|
133
|
+
await this.timeOperation('seed-run', async () => {
|
|
134
|
+
// Global seeding steps i.e across all modules
|
|
135
|
+
if (shouldSeedGlobalMetadata) {
|
|
136
|
+
currentStep = 'seedGlobalMetadata';
|
|
137
|
+
await this.timeOperation('global-seed', () => this.seedGlobalMetadata(), { moduleName: 'global', component: 'global-metadata' });
|
|
138
|
+
} else {
|
|
139
|
+
this.logger.log(`Skipping global metadata seeding.`);
|
|
140
|
+
}
|
|
134
141
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
// Module specific seeding steps.
|
|
143
|
+
// Get all the module metadata files which needs to be seeded.
|
|
144
|
+
const seedDataFiles = this.seedDataFiles;
|
|
145
|
+
this.logger.debug(`Found seed data for modules: ${seedDataFiles.map(s => s.moduleMetadata?.name)}`);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* -------------------------------------------------------------
|
|
149
|
+
* Selective module seeding via: solid seed --modules-to-seed onboarding,reports
|
|
150
|
+
* -------------------------------------------------------------
|
|
151
|
+
*/
|
|
152
|
+
currentStep = 'resolveModulesToSeed';
|
|
153
|
+
if (conf && Array.isArray(conf.modulesToSeed)) {
|
|
154
|
+
modulesToSeed = conf.modulesToSeed;
|
|
155
|
+
console.log(`▶ Selective seeding enabled. Modules to seed: ${modulesToSeed.join(', ')}`);
|
|
156
|
+
this.logger.log(`Selective seeding enabled. Modules to seed: ${modulesToSeed.join(', ')}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`▶ No modulesToSeed provided. Seeding ALL modules.`);
|
|
159
|
+
this.logger.log(`No modulesToSeed provided. Seeding ALL modules.`);
|
|
160
|
+
}
|
|
139
161
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (let i = 0; i < filteredSeedDataFiles.length; i++) {
|
|
143
|
-
const overallMetadata = filteredSeedDataFiles[i];
|
|
144
|
-
const moduleMetadata: CreateModuleMetadataDto = overallMetadata.moduleMetadata;
|
|
145
|
-
currentModule = moduleMetadata?.name ?? 'unknown';
|
|
162
|
+
// Filter modules if needed
|
|
163
|
+
const filteredSeedDataFiles = modulesToSeed ? seedDataFiles.filter((file) => modulesToSeed.includes(file.moduleMetadata?.name)) : seedDataFiles;
|
|
146
164
|
|
|
147
|
-
if (
|
|
148
|
-
this.logger.warn(`
|
|
149
|
-
|
|
165
|
+
if (filteredSeedDataFiles.length === 0) {
|
|
166
|
+
this.logger.warn(`No modules matched the provided modulesToSeed list.`);
|
|
167
|
+
return;
|
|
150
168
|
}
|
|
151
169
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
170
|
+
// let usersDetail;
|
|
171
|
+
// For each module metadata file, we will process the seeding steps one by one.
|
|
172
|
+
for (let i = 0; i < filteredSeedDataFiles.length; i++) {
|
|
173
|
+
const overallMetadata = filteredSeedDataFiles[i];
|
|
174
|
+
const moduleMetadata: CreateModuleMetadataDto = overallMetadata.moduleMetadata;
|
|
175
|
+
currentModule = moduleMetadata?.name ?? 'unknown';
|
|
176
|
+
|
|
177
|
+
if (!moduleMetadata?.name) {
|
|
178
|
+
this.logger.warn(`Skipping seed metadata file because moduleMetadata.name is missing.`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`▶ Seeding Metadata for Module: ${moduleMetadata.name}`);
|
|
183
|
+
this.logger.log(`Seeding Metadata for Module: ${moduleMetadata.name}`);
|
|
184
|
+
|
|
185
|
+
await this.timeOperation('module-total', async () => {
|
|
186
|
+
currentStep = 'seedMediaStorageProviders';
|
|
187
|
+
this.logger.log(`Seeding Media Storage Providers`);
|
|
188
|
+
const mediaStorageCounts = await this.timeOperation('seed-media-storage-providers', () => this.seedMediaStorageProviders(overallMetadata.mediaStorageProviders), { moduleName: moduleMetadata.name, component: 'media-storage-providers' });
|
|
189
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Media Storage Providers', mediaStorageCounts)}`);
|
|
190
|
+
|
|
191
|
+
// Process module metadata first.
|
|
192
|
+
currentStep = 'seedModuleModelFields';
|
|
193
|
+
this.logger.log(`Seeding Module / Model / Fields`);
|
|
194
|
+
const moduleModelFieldCounts = await this.timeOperation('seed-module-model-fields', () => this.seedModuleModelFields(moduleMetadata), { moduleName: moduleMetadata.name, component: 'module-model-fields' });
|
|
195
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Module/Model/Fields', moduleModelFieldCounts)}`);
|
|
196
|
+
|
|
197
|
+
currentStep = 'seedPermissions';
|
|
198
|
+
this.logger.log(`Seeding Permissions`);
|
|
199
|
+
const permissionCounts = await this.timeOperation('seed-permissions', () => this.seedPermissions(overallMetadata), { moduleName: moduleMetadata.name, component: 'permissions' });
|
|
200
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Permissions', permissionCounts)}`);
|
|
201
|
+
|
|
202
|
+
currentStep = 'seedRoles';
|
|
203
|
+
this.logger.log(`Seeding Roles`);
|
|
204
|
+
const roleCounts = await this.timeOperation('seed-roles', () => this.seedRoles(overallMetadata), { moduleName: moduleMetadata.name, component: 'roles' });
|
|
205
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Roles', roleCounts)}`);
|
|
206
|
+
|
|
207
|
+
currentStep = 'seedUsers';
|
|
208
|
+
this.logger.log(`Seeding Users`);
|
|
209
|
+
const userCounts = await this.timeOperation('seed-users', () => this.seedUsers(overallMetadata), { moduleName: moduleMetadata.name, component: 'users' });
|
|
210
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Users', userCounts)}`);
|
|
211
|
+
|
|
212
|
+
currentStep = 'seedViews';
|
|
213
|
+
this.logger.log(`Seeding Views`);
|
|
214
|
+
const viewCounts = await this.timeOperation('seed-views', () => this.seedViews(overallMetadata), { moduleName: moduleMetadata.name, component: 'views' });
|
|
215
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Views', viewCounts)}`);
|
|
216
|
+
|
|
217
|
+
currentStep = 'seedActions';
|
|
218
|
+
this.logger.log(`Seeding Actions`);
|
|
219
|
+
const actionCounts = await this.timeOperation('seed-actions', () => this.seedActions(overallMetadata), { moduleName: moduleMetadata.name, component: 'actions' });
|
|
220
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Actions', actionCounts)}`);
|
|
221
|
+
|
|
222
|
+
currentStep = 'seedMenus';
|
|
223
|
+
this.logger.log(`Seeding Menus`);
|
|
224
|
+
const menuCounts = await this.timeOperation('seed-menus', () => this.seedMenus(overallMetadata), { moduleName: moduleMetadata.name, component: 'menus' });
|
|
225
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Menus', menuCounts)}`);
|
|
226
|
+
|
|
227
|
+
currentStep = 'seedEmailTemplates';
|
|
228
|
+
this.logger.log(`Seeding Email Templates`);
|
|
229
|
+
const emailTemplateCounts = await this.timeOperation('seed-email-templates', () => this.seedEmailTemplates(overallMetadata, moduleMetadata.name), { moduleName: moduleMetadata.name, component: 'email-templates' });
|
|
230
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Email Templates', emailTemplateCounts)}`);
|
|
231
|
+
|
|
232
|
+
currentStep = 'seedSmsTemplates';
|
|
233
|
+
this.logger.log(`Seeding Sms Templates`);
|
|
234
|
+
const smsTemplateCounts = await this.timeOperation('seed-sms-templates', () => this.seedSmsTemplates(overallMetadata, moduleMetadata.name), { moduleName: moduleMetadata.name, component: 'sms-templates' });
|
|
235
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Sms Templates', smsTemplateCounts)}`);
|
|
236
|
+
|
|
237
|
+
currentStep = 'seedSecurityRules';
|
|
238
|
+
this.logger.log(`Seeding Security Rules`);
|
|
239
|
+
const securityRuleCounts = await this.timeOperation('seed-security-rules', () => this.seedSecurityRules(overallMetadata), { moduleName: moduleMetadata.name, component: 'security-rules' });
|
|
240
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Security Rules', securityRuleCounts)}`);
|
|
241
|
+
|
|
242
|
+
currentStep = 'seedListOfValues';
|
|
243
|
+
this.logger.log(`Seeding List Of Values`);
|
|
244
|
+
const lovCounts = await this.timeOperation('seed-list-of-values', () => this.seedListOfValues(moduleMetadata, overallMetadata), { moduleName: moduleMetadata.name, component: 'list-of-values' });
|
|
245
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'List Of Values', lovCounts)}`);
|
|
246
|
+
|
|
247
|
+
currentStep = 'seedScheduledJobs';
|
|
248
|
+
this.logger.log(`Seeding Scheduled Jobs`);
|
|
249
|
+
const scheduledJobCounts = await this.timeOperation('seed-scheduled-jobs', () => this.seedScheduledJobs(moduleMetadata, overallMetadata), { moduleName: moduleMetadata.name, component: 'scheduled-jobs' });
|
|
250
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Scheduled Jobs', scheduledJobCounts)}`);
|
|
251
|
+
|
|
252
|
+
currentStep = 'seedSavedFilters';
|
|
253
|
+
this.logger.log(`Seeding Saved Filters`);
|
|
254
|
+
const savedFilterCounts = await this.timeOperation('seed-saved-filters', () => this.seedSavedFilters(moduleMetadata, overallMetadata), { moduleName: moduleMetadata.name, component: 'saved-filters' });
|
|
255
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Saved Filters', savedFilterCounts)}`);
|
|
256
|
+
|
|
257
|
+
currentStep = 'seedModelSequences';
|
|
258
|
+
this.logger.log(`Seeding Model Sequences`);
|
|
259
|
+
const modelSequenceCounts = await this.timeOperation('seed-model-sequences', () => this.seedModelSequences(overallMetadata), { moduleName: moduleMetadata.name, component: 'model-sequences' });
|
|
260
|
+
console.log(`${this.formatSeedResult(moduleMetadata.name, 'Model Sequences', modelSequenceCounts)}`);
|
|
261
|
+
}, { moduleName: moduleMetadata.name, component: 'module-seed' });
|
|
262
|
+
}
|
|
231
263
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
264
|
+
currentModule = 'global';
|
|
265
|
+
currentStep = 'setupDefaultRolesWithPermissions';
|
|
266
|
+
await this.timeOperation('setup-default-roles-with-permissions', () => this.setupDefaultRolesWithPermissions(), { moduleName: 'global', component: 'default-roles' });
|
|
267
|
+
}, { moduleName: 'global', component: 'seed-run' });
|
|
235
268
|
|
|
236
269
|
// Add a console log indicating seeding is finished. This needs to be console.log so that it looks proper when this code is run via CLI.
|
|
237
270
|
console.log(`✔ Seeding completed.`);
|
|
@@ -250,40 +283,197 @@ export class ModuleMetadataSeederService {
|
|
|
250
283
|
}
|
|
251
284
|
}
|
|
252
285
|
|
|
286
|
+
private buildTimingPrefix(itemTag: string, options?: { moduleName?: string; component?: string; serviceCall?: string }): string {
|
|
287
|
+
const parts = [`[${this.seedTimingTag}]`, `[ITEM:${itemTag}]`];
|
|
288
|
+
if (options?.moduleName) {
|
|
289
|
+
parts.push(`[MODULE:${options.moduleName}]`);
|
|
290
|
+
}
|
|
291
|
+
if (options?.component) {
|
|
292
|
+
parts.push(`[COMPONENT:${options.component}]`);
|
|
293
|
+
}
|
|
294
|
+
if (options?.serviceCall) {
|
|
295
|
+
parts.push(`[CALL:${options.serviceCall}]`);
|
|
296
|
+
}
|
|
297
|
+
return parts.join(' ');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private formatDuration(durationMs: number): string {
|
|
301
|
+
return durationMs >= 1000
|
|
302
|
+
? `${(durationMs / 1000).toFixed(2)} s (${durationMs.toFixed(2)} ms)`
|
|
303
|
+
: `${durationMs.toFixed(2)} ms`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private resetLookupCaches(): void {
|
|
307
|
+
this.moduleLookupCache.clear();
|
|
308
|
+
this.modelLookupCache.clear();
|
|
309
|
+
this.viewLookupCache.clear();
|
|
310
|
+
this.mediaStorageProviderLookupCache.clear();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// These cache-backed helpers now log only cache misses. Earlier versions also logged cache hits and operation
|
|
314
|
+
// starts, but that produced too much verbose noise relative to the debugging value.
|
|
315
|
+
private async getModuleByUserKeyCached(
|
|
316
|
+
moduleUserKey: string,
|
|
317
|
+
options?: { moduleName?: string; component?: string; details?: string },
|
|
318
|
+
): Promise<ModuleMetadata | null> {
|
|
319
|
+
if (!moduleUserKey) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
if (this.moduleLookupCache.has(moduleUserKey)) {
|
|
323
|
+
return this.moduleLookupCache.get(moduleUserKey) ?? null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const module = await this.timeOperation('module-find-by-user-key-cache-miss', () => this.moduleMetadataService.findOneByUserKey(moduleUserKey), {
|
|
327
|
+
moduleName: options?.moduleName ?? moduleUserKey,
|
|
328
|
+
component: options?.component,
|
|
329
|
+
serviceCall: 'moduleMetadataService.findOneByUserKey',
|
|
330
|
+
details: options?.details ?? `module=${moduleUserKey}`,
|
|
331
|
+
});
|
|
332
|
+
this.moduleLookupCache.set(moduleUserKey, module ?? null);
|
|
333
|
+
return module ?? null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async getModelByUserKeyCached(
|
|
337
|
+
modelUserKey: string,
|
|
338
|
+
options?: { moduleName?: string; component?: string; details?: string },
|
|
339
|
+
): Promise<ModelMetadata | null> {
|
|
340
|
+
if (!modelUserKey) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
if (this.modelLookupCache.has(modelUserKey)) {
|
|
344
|
+
return this.modelLookupCache.get(modelUserKey) ?? null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const model = await this.timeOperation('model-find-by-user-key-cache-miss', () => this.modelMetadataService.findOneByUserKey(modelUserKey), {
|
|
348
|
+
moduleName: options?.moduleName,
|
|
349
|
+
component: options?.component,
|
|
350
|
+
serviceCall: 'modelMetadataService.findOneByUserKey',
|
|
351
|
+
details: options?.details ?? `model=${modelUserKey}`,
|
|
352
|
+
});
|
|
353
|
+
this.modelLookupCache.set(modelUserKey, model ?? null);
|
|
354
|
+
return model ?? null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private async getViewByUserKeyCached(
|
|
358
|
+
viewUserKey: string,
|
|
359
|
+
options?: { moduleName?: string; component?: string; details?: string },
|
|
360
|
+
): Promise<ViewMetadata | null> {
|
|
361
|
+
if (!viewUserKey) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (this.viewLookupCache.has(viewUserKey)) {
|
|
365
|
+
return this.viewLookupCache.get(viewUserKey) ?? null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const view = await this.timeOperation('view-find-by-user-key-cache-miss', () => this.solidViewService.findOneByUserKey(viewUserKey), {
|
|
369
|
+
moduleName: options?.moduleName,
|
|
370
|
+
component: options?.component,
|
|
371
|
+
serviceCall: 'solidViewService.findOneByUserKey',
|
|
372
|
+
details: options?.details ?? `view=${viewUserKey}`,
|
|
373
|
+
});
|
|
374
|
+
this.viewLookupCache.set(viewUserKey, view ?? null);
|
|
375
|
+
return view ?? null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async getMediaStorageProviderByUserKeyCached(
|
|
379
|
+
mediaStorageProviderUserKey: string,
|
|
380
|
+
options?: { moduleName?: string; component?: string; details?: string },
|
|
381
|
+
): Promise<any | null> {
|
|
382
|
+
if (!mediaStorageProviderUserKey) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
if (this.mediaStorageProviderLookupCache.has(mediaStorageProviderUserKey)) {
|
|
386
|
+
return this.mediaStorageProviderLookupCache.get(mediaStorageProviderUserKey) ?? null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const mediaStorageProvider = await this.timeOperation('media-storage-provider-find-by-user-key-cache-miss', () => this.mediaStorageProviderMetadataService.findOneByUserKey(mediaStorageProviderUserKey), {
|
|
390
|
+
moduleName: options?.moduleName,
|
|
391
|
+
component: options?.component,
|
|
392
|
+
serviceCall: 'mediaStorageProviderMetadataService.findOneByUserKey',
|
|
393
|
+
details: options?.details ?? `provider=${mediaStorageProviderUserKey}`,
|
|
394
|
+
});
|
|
395
|
+
this.mediaStorageProviderLookupCache.set(mediaStorageProviderUserKey, mediaStorageProvider ?? null);
|
|
396
|
+
return mediaStorageProvider ?? null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async timeOperation<T>(
|
|
400
|
+
itemTag: string,
|
|
401
|
+
operation: () => Promise<T>,
|
|
402
|
+
options?: { moduleName?: string; component?: string; serviceCall?: string; details?: string },
|
|
403
|
+
): Promise<T> {
|
|
404
|
+
const prefix = this.buildTimingPrefix(itemTag, options);
|
|
405
|
+
const detailSuffix = options?.details ? ` ${options.details}` : '';
|
|
406
|
+
const start = process.hrtime.bigint();
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const result = await operation();
|
|
410
|
+
const durationMs = Number(process.hrtime.bigint() - start) / 1_000_000;
|
|
411
|
+
this.logger.debug(`${prefix} done in ${this.formatDuration(durationMs)}${detailSuffix}`);
|
|
412
|
+
return result;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const durationMs = Number(process.hrtime.bigint() - start) / 1_000_000;
|
|
415
|
+
this.logger.debug(`${prefix} failed after ${this.formatDuration(durationMs)}${detailSuffix}`);
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
253
420
|
private async seedScheduledJobs(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
254
|
-
this.logger.debug(`[Start] Processing scheduled jobs for ${moduleMetadata.name}`);
|
|
255
421
|
const scheduledJobs = this.getSeedArray<CreateScheduledJobDto>(overallMetadata?.scheduledJobs);
|
|
256
|
-
const pruned = this.enablePruning ? await this.pruneScheduledJobs(scheduledJobs, moduleMetadata.name)
|
|
422
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-scheduled-jobs', () => this.pruneScheduledJobs(scheduledJobs, moduleMetadata.name), {
|
|
423
|
+
moduleName: moduleMetadata.name,
|
|
424
|
+
component: 'scheduled-jobs',
|
|
425
|
+
serviceCall: 'pruneScheduledJobs',
|
|
426
|
+
}) : 0;
|
|
257
427
|
if (scheduledJobs.length > 0) {
|
|
258
|
-
await this.handleSeedScheduledJobs(scheduledJobs)
|
|
428
|
+
await this.timeOperation('handle-scheduled-jobs', () => this.handleSeedScheduledJobs(scheduledJobs), {
|
|
429
|
+
moduleName: moduleMetadata.name,
|
|
430
|
+
component: 'scheduled-jobs',
|
|
431
|
+
serviceCall: 'handleSeedScheduledJobs',
|
|
432
|
+
});
|
|
259
433
|
}
|
|
260
|
-
this.logger.debug(`[End] Processing scheduled jobs for ${moduleMetadata.name}`);
|
|
261
434
|
return { pruned, upserted: scheduledJobs.length };
|
|
262
435
|
}
|
|
263
436
|
|
|
264
437
|
private async seedSavedFilters(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
265
|
-
this.logger.debug(`[Start] Processing saved filters for ${moduleMetadata.name}`);
|
|
266
438
|
const savedFilters = this.getSeedArray<CreateSavedFiltersDto>(overallMetadata?.savedFilters);
|
|
267
|
-
const pruned = this.enablePruning ? await this.pruneSavedFilters(savedFilters, moduleMetadata.name)
|
|
439
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-saved-filters', () => this.pruneSavedFilters(savedFilters, moduleMetadata.name), {
|
|
440
|
+
moduleName: moduleMetadata.name,
|
|
441
|
+
component: 'saved-filters',
|
|
442
|
+
serviceCall: 'pruneSavedFilters',
|
|
443
|
+
}) : 0;
|
|
268
444
|
if (savedFilters.length > 0) {
|
|
269
|
-
await this.handleSeedSavedFilters(savedFilters)
|
|
445
|
+
await this.timeOperation('handle-saved-filters', () => this.handleSeedSavedFilters(savedFilters), {
|
|
446
|
+
moduleName: moduleMetadata.name,
|
|
447
|
+
component: 'saved-filters',
|
|
448
|
+
serviceCall: 'handleSeedSavedFilters',
|
|
449
|
+
});
|
|
270
450
|
}
|
|
271
|
-
this.logger.debug(`[End] Processing saved filters for ${moduleMetadata.name}`);
|
|
272
451
|
return { pruned, upserted: savedFilters.length };
|
|
273
452
|
}
|
|
274
453
|
|
|
275
454
|
private async seedListOfValues(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
276
|
-
this.logger.debug(`[Start] Processing List Of Values for ${moduleMetadata.name}`);
|
|
277
455
|
const listOfValues = this.getSeedArray<CreateListOfValuesDto>(overallMetadata?.listOfValues);
|
|
278
|
-
const pruned = this.enablePruning ? await this.pruneListOfValues(listOfValues, moduleMetadata.name)
|
|
279
|
-
|
|
280
|
-
|
|
456
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-list-of-values', () => this.pruneListOfValues(listOfValues, moduleMetadata.name), {
|
|
457
|
+
moduleName: moduleMetadata.name,
|
|
458
|
+
component: 'list-of-values',
|
|
459
|
+
serviceCall: 'pruneListOfValues',
|
|
460
|
+
}) : 0;
|
|
461
|
+
await this.timeOperation('handle-list-of-values', () => this.handleSeedListOfValues(listOfValues), {
|
|
462
|
+
moduleName: moduleMetadata.name,
|
|
463
|
+
component: 'list-of-values',
|
|
464
|
+
serviceCall: 'handleSeedListOfValues',
|
|
465
|
+
});
|
|
281
466
|
return { pruned, upserted: listOfValues.length };
|
|
282
467
|
}
|
|
283
468
|
|
|
284
469
|
private async setupDefaultRolesWithPermissions() {
|
|
285
470
|
this.logger.debug(`About to add all permissions to the Admin role`);
|
|
286
|
-
await this.roleService.addAllPermissionsToRole(ADMIN_ROLE_NAME)
|
|
471
|
+
await this.timeOperation('role-add-all-permissions', () => this.roleService.addAllPermissionsToRole(ADMIN_ROLE_NAME), {
|
|
472
|
+
moduleName: 'global',
|
|
473
|
+
component: 'default-roles',
|
|
474
|
+
serviceCall: 'roleService.addAllPermissionsToRole',
|
|
475
|
+
details: `role=${ADMIN_ROLE_NAME}`,
|
|
476
|
+
});
|
|
287
477
|
|
|
288
478
|
// The below code is commented out for now as we are including permissions for these roles from the seeder json for the Internal and Public role.
|
|
289
479
|
// 2. Give permissions to the Internal / Public role.
|
|
@@ -295,109 +485,151 @@ export class ModuleMetadataSeederService {
|
|
|
295
485
|
}
|
|
296
486
|
|
|
297
487
|
private async seedSecurityRules(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
298
|
-
this.logger.debug(`[Start] Processing security rules`);
|
|
299
488
|
const securityRules = this.getSeedArray<CreateSecurityRuleDto>(overallMetadata?.securityRules);
|
|
300
|
-
const pruned = this.enablePruning ? await this.pruneSecurityRules(securityRules, overallMetadata?.moduleMetadata?.name)
|
|
301
|
-
|
|
302
|
-
|
|
489
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-security-rules', () => this.pruneSecurityRules(securityRules, overallMetadata?.moduleMetadata?.name), {
|
|
490
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
491
|
+
component: 'security-rules',
|
|
492
|
+
serviceCall: 'pruneSecurityRules',
|
|
493
|
+
}) : 0;
|
|
494
|
+
await this.timeOperation('handle-security-rules', () => this.handleSeedSecurityRules(securityRules), {
|
|
495
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
496
|
+
component: 'security-rules',
|
|
497
|
+
serviceCall: 'handleSeedSecurityRules',
|
|
498
|
+
});
|
|
303
499
|
return { pruned, upserted: securityRules.length };
|
|
304
500
|
}
|
|
305
501
|
|
|
306
502
|
// Ok
|
|
307
503
|
private async seedDefaultSettings() {
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
|
|
504
|
+
await this.timeOperation('seed-default-settings-call', () => this.settingService.seedSystemAdminEditableAndAboveSettings(), {
|
|
505
|
+
moduleName: 'global',
|
|
506
|
+
component: 'settings',
|
|
507
|
+
serviceCall: 'settingService.seedSystemAdminEditableAndAboveSettings',
|
|
508
|
+
});
|
|
311
509
|
}
|
|
312
510
|
|
|
313
511
|
private async seedSmsTemplates(overallMetadata: any, moduleName: string): Promise<{ pruned: number; upserted: number }> {
|
|
314
|
-
this.logger.debug(`[Start] Processing sms templates`);
|
|
315
512
|
const smsTemplates = this.getSeedArray<CreateSmsTemplateDto>(overallMetadata?.smsTemplates);
|
|
316
|
-
await this.handleSeedSmsTemplates(smsTemplates, moduleName)
|
|
317
|
-
|
|
513
|
+
await this.timeOperation('handle-sms-templates', () => this.handleSeedSmsTemplates(smsTemplates, moduleName), {
|
|
514
|
+
moduleName,
|
|
515
|
+
component: 'sms-templates',
|
|
516
|
+
serviceCall: 'handleSeedSmsTemplates',
|
|
517
|
+
});
|
|
318
518
|
return { pruned: 0, upserted: smsTemplates.length };
|
|
319
519
|
}
|
|
320
520
|
|
|
321
521
|
// OK
|
|
322
522
|
private async seedEmailTemplates(overallMetadata: any, moduleName: string): Promise<{ pruned: number; upserted: number }> {
|
|
323
|
-
this.logger.debug(`[Start] Processing email templates`);
|
|
324
523
|
const emailTemplates = this.getSeedArray<CreateEmailTemplateDto>(overallMetadata?.emailTemplates);
|
|
325
|
-
await this.handleSeedEmailTemplates(emailTemplates, moduleName)
|
|
326
|
-
|
|
524
|
+
await this.timeOperation('handle-email-templates', () => this.handleSeedEmailTemplates(emailTemplates, moduleName), {
|
|
525
|
+
moduleName,
|
|
526
|
+
component: 'email-templates',
|
|
527
|
+
serviceCall: 'handleSeedEmailTemplates',
|
|
528
|
+
});
|
|
327
529
|
return { pruned: 0, upserted: emailTemplates.length };
|
|
328
530
|
}
|
|
329
531
|
|
|
330
532
|
// Ok
|
|
331
533
|
private async seedMenus(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
332
|
-
this.logger.debug(`[Start] Processing menus`);
|
|
333
534
|
const menus = this.getSeedArray<any>(overallMetadata?.menus);
|
|
334
|
-
const pruned = this.enablePruning ? await this.pruneMenus(menus, overallMetadata?.moduleMetadata?.name)
|
|
335
|
-
|
|
336
|
-
|
|
535
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-menus', () => this.pruneMenus(menus, overallMetadata?.moduleMetadata?.name), {
|
|
536
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
537
|
+
component: 'menus',
|
|
538
|
+
serviceCall: 'pruneMenus',
|
|
539
|
+
}) : 0;
|
|
540
|
+
await this.timeOperation('handle-menus', () => this.handleSeedMenus(menus), {
|
|
541
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
542
|
+
component: 'menus',
|
|
543
|
+
serviceCall: 'handleSeedMenus',
|
|
544
|
+
});
|
|
337
545
|
return { pruned, upserted: menus.length };
|
|
338
546
|
}
|
|
339
547
|
|
|
340
548
|
// Ok
|
|
341
549
|
private async seedActions(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
342
|
-
this.logger.debug(`[Start] Processing actions`);
|
|
343
550
|
const actions = this.getSeedArray<any>(overallMetadata?.actions);
|
|
344
|
-
const pruned = this.enablePruning ? await this.pruneActions(actions, overallMetadata?.moduleMetadata?.name)
|
|
345
|
-
|
|
346
|
-
|
|
551
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-actions', () => this.pruneActions(actions, overallMetadata?.moduleMetadata?.name), {
|
|
552
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
553
|
+
component: 'actions',
|
|
554
|
+
serviceCall: 'pruneActions',
|
|
555
|
+
}) : 0;
|
|
556
|
+
await this.timeOperation('handle-actions', () => this.handleSeedActions(actions), {
|
|
557
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
558
|
+
component: 'actions',
|
|
559
|
+
serviceCall: 'handleSeedActions',
|
|
560
|
+
});
|
|
347
561
|
return { pruned, upserted: actions.length };
|
|
348
562
|
}
|
|
349
563
|
|
|
350
564
|
// Ok
|
|
351
565
|
private async seedViews(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
352
|
-
this.logger.debug(`[Start] Processing views`);
|
|
353
566
|
const views = this.getSeedArray<any>(overallMetadata?.views);
|
|
354
|
-
const pruned = this.enablePruning ? await this.pruneViews(views, overallMetadata?.moduleMetadata?.name)
|
|
355
|
-
|
|
356
|
-
|
|
567
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-views', () => this.pruneViews(views, overallMetadata?.moduleMetadata?.name), {
|
|
568
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
569
|
+
component: 'views',
|
|
570
|
+
serviceCall: 'pruneViews',
|
|
571
|
+
}) : 0;
|
|
572
|
+
await this.timeOperation('handle-views', () => this.handleSeedViews(views), {
|
|
573
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
574
|
+
component: 'views',
|
|
575
|
+
serviceCall: 'handleSeedViews',
|
|
576
|
+
});
|
|
357
577
|
return { pruned, upserted: views.length };
|
|
358
578
|
}
|
|
359
579
|
|
|
360
580
|
// Ok
|
|
361
581
|
private async seedUsers(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
362
|
-
this.logger.debug(`[Start] Processing users`);
|
|
363
582
|
const users = this.getSeedArray<SignUpDto>(overallMetadata?.users);
|
|
364
583
|
// usersDetail = users;
|
|
365
|
-
await this.handleSeedUsers(users)
|
|
366
|
-
|
|
584
|
+
await this.timeOperation('handle-users', () => this.handleSeedUsers(users), {
|
|
585
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
586
|
+
component: 'users',
|
|
587
|
+
serviceCall: 'handleSeedUsers',
|
|
588
|
+
});
|
|
367
589
|
return { pruned: 0, upserted: users.length };
|
|
368
590
|
}
|
|
369
591
|
|
|
370
592
|
// OK
|
|
371
593
|
private async seedRoles(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
372
|
-
this.logger.debug(`[Start] Processing roles`);
|
|
373
594
|
const roles = this.getSeedArray<CreateRoleMetadataDto>(overallMetadata?.roles);
|
|
374
595
|
// While creating roles we are only passing the role name to be used.
|
|
375
|
-
await this.roleService.createRolesIfNotExists(
|
|
596
|
+
await this.timeOperation('roles-create-if-not-exists', () => this.roleService.createRolesIfNotExists(
|
|
376
597
|
roles
|
|
377
598
|
.filter((role) => role?.name)
|
|
378
599
|
.map((role) => ({ name: role.name } as any)),
|
|
379
|
-
)
|
|
600
|
+
), {
|
|
601
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
602
|
+
component: 'roles',
|
|
603
|
+
serviceCall: 'roleService.createRolesIfNotExists',
|
|
604
|
+
details: `roleCount=${roles.length}`,
|
|
605
|
+
});
|
|
380
606
|
// After roles are created, we iterate over all roles and attach permissions (if specified in the seeder json) to the respective role.
|
|
381
607
|
// Every role configuration in the seeder json can optionally have a permissions attribute.
|
|
382
608
|
for (const role of roles) {
|
|
383
609
|
if (role.permissions) {
|
|
384
|
-
await this.roleService.addPermissionsToRole(
|
|
610
|
+
await this.timeOperation('role-add-permissions', () => this.roleService.addPermissionsToRole(
|
|
385
611
|
role.name,
|
|
386
612
|
role.permissions
|
|
387
613
|
.map((permission: any) => typeof permission === 'string' ? permission : permission?.name)
|
|
388
614
|
.filter(Boolean),
|
|
389
|
-
)
|
|
615
|
+
), {
|
|
616
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
617
|
+
component: 'roles',
|
|
618
|
+
serviceCall: 'roleService.addPermissionsToRole',
|
|
619
|
+
details: `role=${role.name} permissionCount=${role.permissions.length}`,
|
|
620
|
+
});
|
|
390
621
|
}
|
|
391
622
|
}
|
|
392
|
-
this.logger.debug(`[End] Processing roles`);
|
|
393
623
|
return { pruned: 0, upserted: roles.length };
|
|
394
624
|
}
|
|
395
625
|
|
|
396
626
|
private async seedPermissions(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
397
|
-
this.logger.debug(`[Start] Processing permissions`);
|
|
398
627
|
const permissions = overallMetadata.permissions ?? [];
|
|
399
|
-
await this.handleSeedPermissions(permissions)
|
|
400
|
-
|
|
628
|
+
await this.timeOperation('handle-permissions', () => this.handleSeedPermissions(permissions), {
|
|
629
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
630
|
+
component: 'permissions',
|
|
631
|
+
serviceCall: 'handleSeedPermissions',
|
|
632
|
+
});
|
|
401
633
|
return { pruned: 0, upserted: permissions?.length ?? 0 };
|
|
402
634
|
}
|
|
403
635
|
|
|
@@ -423,24 +655,37 @@ export class ModuleMetadataSeederService {
|
|
|
423
655
|
// OK
|
|
424
656
|
private async seedGlobalMetadata() {
|
|
425
657
|
this.logger.log(`Seeding Permissions`);
|
|
426
|
-
await this.seedControllerPermissions()
|
|
658
|
+
await this.timeOperation('seed-controller-permissions', () => this.seedControllerPermissions(), {
|
|
659
|
+
moduleName: 'global',
|
|
660
|
+
component: 'controller-permissions',
|
|
661
|
+
});
|
|
427
662
|
|
|
428
663
|
// this.logger.log(`Seeding Default Media Storage Providers`);
|
|
429
664
|
// await this.seedDefaultMediaStorageProviders();
|
|
430
665
|
|
|
431
666
|
this.logger.log(`Seeding System Fields Metadata`);
|
|
432
|
-
await this.seedDefaultSystemFields()
|
|
667
|
+
await this.timeOperation('seed-default-system-fields', () => this.seedDefaultSystemFields(), {
|
|
668
|
+
moduleName: 'global',
|
|
669
|
+
component: 'system-fields',
|
|
670
|
+
});
|
|
433
671
|
|
|
434
672
|
// Settings
|
|
435
673
|
this.logger.log(`Seeding Default Settings`);
|
|
436
|
-
await this.seedDefaultSettings()
|
|
674
|
+
await this.timeOperation('seed-default-settings', () => this.seedDefaultSettings(), {
|
|
675
|
+
moduleName: 'global',
|
|
676
|
+
component: 'settings',
|
|
677
|
+
});
|
|
437
678
|
|
|
438
679
|
|
|
439
680
|
this.logger.debug(`Global metadata seeding completed`);
|
|
440
681
|
}
|
|
441
682
|
|
|
442
683
|
private async seedDefaultSystemFields() {
|
|
443
|
-
await this.systemFieldsSeederService.seed()
|
|
684
|
+
await this.timeOperation('system-fields-seed', () => this.systemFieldsSeederService.seed(), {
|
|
685
|
+
moduleName: 'global',
|
|
686
|
+
component: 'system-fields',
|
|
687
|
+
serviceCall: 'systemFieldsSeederService.seed',
|
|
688
|
+
});
|
|
444
689
|
}
|
|
445
690
|
|
|
446
691
|
// OK
|
|
@@ -461,8 +706,6 @@ export class ModuleMetadataSeederService {
|
|
|
461
706
|
const methodName = methods[mId];
|
|
462
707
|
const permissionName = `${controller.name}.${methodName}`;
|
|
463
708
|
permissionNames.push(permissionName);
|
|
464
|
-
|
|
465
|
-
await this.createPermissionIfNotExists(permissionName);
|
|
466
709
|
}
|
|
467
710
|
|
|
468
711
|
} catch (error: any) {
|
|
@@ -470,35 +713,112 @@ export class ModuleMetadataSeederService {
|
|
|
470
713
|
}
|
|
471
714
|
}
|
|
472
715
|
|
|
716
|
+
await this.timeOperation('seed-controller-permissions-bulk', () => this.seedPermissionsInBatches(permissionNames, {
|
|
717
|
+
moduleName: 'global',
|
|
718
|
+
component: 'controller-permissions',
|
|
719
|
+
}), {
|
|
720
|
+
moduleName: 'global',
|
|
721
|
+
component: 'controller-permissions',
|
|
722
|
+
serviceCall: 'seedPermissionsInBatches',
|
|
723
|
+
details: `permissionCount=${permissionNames.length} batchSize=${this.permissionSeedBatchSize}`,
|
|
724
|
+
});
|
|
725
|
+
|
|
473
726
|
if (this.enablePruning) {
|
|
474
|
-
await this.prunePermissions(permissionNames)
|
|
727
|
+
await this.timeOperation('prune-controller-permissions', () => this.prunePermissions(permissionNames), {
|
|
728
|
+
moduleName: 'global',
|
|
729
|
+
component: 'controller-permissions',
|
|
730
|
+
serviceCall: 'prunePermissions',
|
|
731
|
+
});
|
|
475
732
|
}
|
|
476
733
|
}
|
|
477
734
|
|
|
478
|
-
private async
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
735
|
+
private async handleSeedPermissions(permissions: any[]): Promise<void> {
|
|
736
|
+
const permissionNames = permissions
|
|
737
|
+
.map((permission) => typeof permission === 'string' ? permission : permission?.name)
|
|
738
|
+
.filter(Boolean);
|
|
739
|
+
|
|
740
|
+
// This path replaced the old "check one permission at a time" approach that was responsible for one of the
|
|
741
|
+
// biggest seed-time regressions. We now normalize upfront and let the batch helper do set-based existence work.
|
|
742
|
+
await this.seedPermissionsInBatches(permissionNames, {
|
|
743
|
+
component: 'permissions',
|
|
483
744
|
});
|
|
745
|
+
}
|
|
484
746
|
|
|
485
|
-
|
|
486
|
-
|
|
747
|
+
private async seedPermissionsInBatches(
|
|
748
|
+
permissionNames: string[],
|
|
749
|
+
options?: { moduleName?: string; component?: string },
|
|
750
|
+
): Promise<void> {
|
|
751
|
+
const uniquePermissionNames = this.getUniquePermissionNames(permissionNames);
|
|
752
|
+
if (uniquePermissionNames.length === 0) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
487
755
|
|
|
488
|
-
|
|
489
|
-
|
|
756
|
+
// We intentionally process in bounded chunks so we get the performance benefit of set-based reads/writes
|
|
757
|
+
// without loading an unbounded permission list into a single query or save payload.
|
|
758
|
+
for (let index = 0; index < uniquePermissionNames.length; index += this.permissionSeedBatchSize) {
|
|
759
|
+
const batch = uniquePermissionNames.slice(index, index + this.permissionSeedBatchSize);
|
|
760
|
+
const batchNumber = Math.floor(index / this.permissionSeedBatchSize) + 1;
|
|
761
|
+
|
|
762
|
+
const existingPermissions = await this.timeOperation('permission-batch-find', () => this.permissionRepo.find({
|
|
763
|
+
where: {
|
|
764
|
+
name: In(batch),
|
|
765
|
+
}
|
|
766
|
+
}), {
|
|
767
|
+
moduleName: options?.moduleName,
|
|
768
|
+
component: options?.component ?? 'permissions',
|
|
769
|
+
serviceCall: 'permissionRepo.find',
|
|
770
|
+
details: `batch=${batchNumber} batchSize=${batch.length}`,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const existingPermissionNames = new Set(
|
|
774
|
+
existingPermissions.map((permission) => this.normalizePermissionName(permission.name)),
|
|
775
|
+
);
|
|
776
|
+
const missingPermissionNames = batch.filter(
|
|
777
|
+
(name) => !existingPermissionNames.has(this.normalizePermissionName(name)),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
if (missingPermissionNames.length === 0) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
this.logger.log(`Creating ${missingPermissionNames.length} missing permissions in batch ${batchNumber}.`);
|
|
785
|
+
await this.timeOperation('permission-batch-save', () => this.permissionRepo.save(
|
|
786
|
+
missingPermissionNames.map((name) => this.permissionRepo.create({ name })),
|
|
787
|
+
), {
|
|
788
|
+
moduleName: options?.moduleName,
|
|
789
|
+
component: options?.component ?? 'permissions',
|
|
790
|
+
serviceCall: 'permissionRepo.save',
|
|
791
|
+
details: `batch=${batchNumber} createCount=${missingPermissionNames.length}`,
|
|
490
792
|
});
|
|
491
|
-
await this.permissionRepo.save(newPermission);
|
|
492
793
|
}
|
|
493
794
|
}
|
|
494
795
|
|
|
495
|
-
private
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
796
|
+
private getUniquePermissionNames(permissionNames: string[]): string[] {
|
|
797
|
+
const normalizedPermissionNames = new Set<string>();
|
|
798
|
+
const uniquePermissionNames: string[] = [];
|
|
799
|
+
|
|
800
|
+
for (const permissionName of permissionNames) {
|
|
801
|
+
if (!permissionName) {
|
|
802
|
+
continue;
|
|
500
803
|
}
|
|
804
|
+
|
|
805
|
+
// Deduping is intentionally case-insensitive because MSSQL commonly compares these names using a
|
|
806
|
+
// case-insensitive collation. Without that, a batch can think `Foo.Bar` is missing even when
|
|
807
|
+
// `foo.bar` already exists in the database.
|
|
808
|
+
const normalizedPermissionName = this.normalizePermissionName(permissionName);
|
|
809
|
+
if (normalizedPermissionNames.has(normalizedPermissionName)) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
normalizedPermissionNames.add(normalizedPermissionName);
|
|
814
|
+
uniquePermissionNames.push(permissionName.trim());
|
|
501
815
|
}
|
|
816
|
+
|
|
817
|
+
return uniquePermissionNames;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private normalizePermissionName(permissionName: string): string {
|
|
821
|
+
return permissionName.trim().toLowerCase();
|
|
502
822
|
}
|
|
503
823
|
|
|
504
824
|
// OK
|
|
@@ -508,23 +828,31 @@ export class ModuleMetadataSeederService {
|
|
|
508
828
|
|
|
509
829
|
// OK
|
|
510
830
|
private async seedMediaStorageProviders(mediaStorageProviders: any[]): Promise<{ pruned: number; upserted: number }> {
|
|
511
|
-
this.logger.debug(`[Start] Processing Media Storage Provider`);
|
|
512
831
|
const providers = this.getSeedArray<any>(mediaStorageProviders);
|
|
513
832
|
|
|
514
833
|
for (let i = 0; i < providers.length; i++) {
|
|
515
834
|
const mediaStorageProvider = providers[i];
|
|
516
|
-
await this.mediaStorageProviderMetadataService.upsert(mediaStorageProvider)
|
|
835
|
+
await this.timeOperation('media-storage-provider-upsert', () => this.mediaStorageProviderMetadataService.upsert(mediaStorageProvider), {
|
|
836
|
+
component: 'media-storage-providers',
|
|
837
|
+
serviceCall: 'mediaStorageProviderMetadataService.upsert',
|
|
838
|
+
details: `provider=${mediaStorageProvider?.name ?? mediaStorageProvider?.userKey ?? i}`,
|
|
839
|
+
});
|
|
517
840
|
}
|
|
518
|
-
this.logger.debug(`[End] Processing Media Storage Provider`);
|
|
519
841
|
return { pruned: 0, upserted: providers.length };
|
|
520
842
|
}
|
|
521
843
|
|
|
522
844
|
private async seedModelSequences(overallMetadata: any): Promise<{ pruned: number; upserted: number }> {
|
|
523
|
-
this.logger.debug(`[Start] Processing model sequences`);
|
|
524
845
|
const modelSequences = this.getSeedArray<CreateModelSequenceDto>(overallMetadata?.modelSequences);
|
|
525
|
-
const pruned = this.enablePruning ? await this.pruneModelSequences(modelSequences, overallMetadata?.moduleMetadata?.name)
|
|
526
|
-
|
|
527
|
-
|
|
846
|
+
const pruned = this.enablePruning ? await this.timeOperation('prune-model-sequences', () => this.pruneModelSequences(modelSequences, overallMetadata?.moduleMetadata?.name), {
|
|
847
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
848
|
+
component: 'model-sequences',
|
|
849
|
+
serviceCall: 'pruneModelSequences',
|
|
850
|
+
}) : 0;
|
|
851
|
+
await this.timeOperation('handle-model-sequences', () => this.handleSeedModelSequences(modelSequences), {
|
|
852
|
+
moduleName: overallMetadata?.moduleMetadata?.name,
|
|
853
|
+
component: 'model-sequences',
|
|
854
|
+
serviceCall: 'handleSeedModelSequences',
|
|
855
|
+
});
|
|
528
856
|
return { pruned, upserted: modelSequences.length };
|
|
529
857
|
}
|
|
530
858
|
|
|
@@ -588,8 +916,18 @@ export class ModuleMetadataSeederService {
|
|
|
588
916
|
}
|
|
589
917
|
|
|
590
918
|
// Save to DB.
|
|
591
|
-
await this.emailTemplateService.removeByName(emailTemplate.name)
|
|
592
|
-
|
|
919
|
+
await this.timeOperation('email-template-remove', () => this.emailTemplateService.removeByName(emailTemplate.name), {
|
|
920
|
+
moduleName,
|
|
921
|
+
component: 'email-templates',
|
|
922
|
+
serviceCall: 'emailTemplateService.removeByName',
|
|
923
|
+
details: `template=${emailTemplate.name}`,
|
|
924
|
+
});
|
|
925
|
+
await this.timeOperation('email-template-create', () => this.emailTemplateService.create(emailTemplate), {
|
|
926
|
+
moduleName,
|
|
927
|
+
component: 'email-templates',
|
|
928
|
+
serviceCall: 'emailTemplateService.create',
|
|
929
|
+
details: `template=${emailTemplate.name}`,
|
|
930
|
+
});
|
|
593
931
|
}
|
|
594
932
|
|
|
595
933
|
}
|
|
@@ -655,8 +993,18 @@ export class ModuleMetadataSeederService {
|
|
|
655
993
|
|
|
656
994
|
|
|
657
995
|
// Save to DB.
|
|
658
|
-
await this.smsTemplateService.removeByName(smsTemplate.name)
|
|
659
|
-
|
|
996
|
+
await this.timeOperation('sms-template-remove', () => this.smsTemplateService.removeByName(smsTemplate.name), {
|
|
997
|
+
moduleName,
|
|
998
|
+
component: 'sms-templates',
|
|
999
|
+
serviceCall: 'smsTemplateService.removeByName',
|
|
1000
|
+
details: `template=${smsTemplate.name}`,
|
|
1001
|
+
});
|
|
1002
|
+
await this.timeOperation('sms-template-create', () => this.smsTemplateService.create(smsTemplate), {
|
|
1003
|
+
moduleName,
|
|
1004
|
+
component: 'sms-templates',
|
|
1005
|
+
serviceCall: 'smsTemplateService.create',
|
|
1006
|
+
details: `template=${smsTemplate.name}`,
|
|
1007
|
+
});
|
|
660
1008
|
}
|
|
661
1009
|
}
|
|
662
1010
|
|
|
@@ -666,7 +1014,10 @@ export class ModuleMetadataSeederService {
|
|
|
666
1014
|
return;
|
|
667
1015
|
}
|
|
668
1016
|
|
|
669
|
-
|
|
1017
|
+
// Menu writes are now no-op aware, but this block still performs several serial relation lookups per menu.
|
|
1018
|
+
// That tradeoff is acceptable for now because the highest-value wins were elsewhere, but if menu seeding
|
|
1019
|
+
// becomes the next hotspot the natural next step is bulk-preloading actions/modules/existing menus here too.
|
|
1020
|
+
await this.timeOperation('menus-transaction', () => this.dataSource.transaction(async (trx) => {
|
|
670
1021
|
const menuRepo = trx.getRepository(MenuItemMetadata);
|
|
671
1022
|
const roleRepo = trx.getRepository(RoleMetadata);
|
|
672
1023
|
const actionRepo = trx.getRepository(ActionMetadata);
|
|
@@ -675,21 +1026,41 @@ export class ModuleMetadataSeederService {
|
|
|
675
1026
|
// 1) Upsert menus WITHOUT roles (manual upsert)
|
|
676
1027
|
for (const m of menus) {
|
|
677
1028
|
const action = m.actionUserKey
|
|
678
|
-
? await actionRepo.findOne({ where: { name: m.actionUserKey }, select: ["id"] })
|
|
1029
|
+
? await this.timeOperation('menu-action-find-one', () => actionRepo.findOne({ where: { name: m.actionUserKey }, select: ["id"] }), {
|
|
1030
|
+
component: 'menus',
|
|
1031
|
+
serviceCall: 'actionRepo.findOne',
|
|
1032
|
+
details: `action=${m.actionUserKey}`,
|
|
1033
|
+
})
|
|
679
1034
|
: null;
|
|
680
1035
|
|
|
681
1036
|
const module = m.moduleUserKey
|
|
682
|
-
? await moduleRepo.findOne({ where: { name: m.moduleUserKey }, select: ["id"] })
|
|
1037
|
+
? await this.timeOperation('menu-module-find-one', () => moduleRepo.findOne({ where: { name: m.moduleUserKey }, select: ["id"] }), {
|
|
1038
|
+
component: 'menus',
|
|
1039
|
+
serviceCall: 'moduleRepo.findOne',
|
|
1040
|
+
details: `module=${m.moduleUserKey}`,
|
|
1041
|
+
})
|
|
683
1042
|
: null;
|
|
684
1043
|
|
|
685
1044
|
const parentMenuItem = m.parentMenuItemUserKey
|
|
686
|
-
? await menuRepo.findOne({ where: { name: m.parentMenuItemUserKey }, select: ["id"] })
|
|
1045
|
+
? await this.timeOperation('menu-parent-find-one', () => menuRepo.findOne({ where: { name: m.parentMenuItemUserKey }, select: ["id"] }), {
|
|
1046
|
+
component: 'menus',
|
|
1047
|
+
serviceCall: 'menuRepo.findOne',
|
|
1048
|
+
details: `parent=${m.parentMenuItemUserKey}`,
|
|
1049
|
+
})
|
|
687
1050
|
: null;
|
|
688
1051
|
|
|
689
1052
|
// Check if a menu with this name already exists
|
|
690
|
-
const existing = await menuRepo.findOne({
|
|
1053
|
+
const existing = await this.timeOperation('menu-existing-find-one', () => menuRepo.findOne({
|
|
691
1054
|
where: { name: m.name },
|
|
692
|
-
|
|
1055
|
+
relations: {
|
|
1056
|
+
action: true,
|
|
1057
|
+
module: true,
|
|
1058
|
+
parentMenuItem: true,
|
|
1059
|
+
},
|
|
1060
|
+
}), {
|
|
1061
|
+
component: 'menus',
|
|
1062
|
+
serviceCall: 'menuRepo.findOne',
|
|
1063
|
+
details: `menu=${m.name}`,
|
|
693
1064
|
});
|
|
694
1065
|
|
|
695
1066
|
// Build the entity data (without id)
|
|
@@ -703,30 +1074,60 @@ export class ModuleMetadataSeederService {
|
|
|
703
1074
|
iconName: m.iconName,
|
|
704
1075
|
};
|
|
705
1076
|
|
|
1077
|
+
const hasChanges = existing
|
|
1078
|
+
? existing.displayName !== base.displayName
|
|
1079
|
+
|| existing.sequenceNumber !== base.sequenceNumber
|
|
1080
|
+
|| existing.iconName !== base.iconName
|
|
1081
|
+
|| existing.action?.id !== base.action?.id
|
|
1082
|
+
|| existing.module?.id !== base.module?.id
|
|
1083
|
+
|| existing.parentMenuItem?.id !== base.parentMenuItem?.id
|
|
1084
|
+
: true;
|
|
1085
|
+
|
|
1086
|
+
if (existing && !hasChanges) {
|
|
1087
|
+
this.logger.debug(`Skipping menu upsert for ${m.name}; no changes detected.`);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
706
1091
|
// If existing, set its id so save() will perform an update, otherwise insert
|
|
707
1092
|
const entity = menuRepo.create(
|
|
708
1093
|
existing ? { id: existing.id, ...base } : base,
|
|
709
1094
|
);
|
|
710
1095
|
|
|
711
|
-
await menuRepo.save(entity)
|
|
1096
|
+
await this.timeOperation('menu-save', () => menuRepo.save(entity), {
|
|
1097
|
+
component: 'menus',
|
|
1098
|
+
serviceCall: 'menuRepo.save',
|
|
1099
|
+
details: `menu=${m.name}`,
|
|
1100
|
+
});
|
|
712
1101
|
}
|
|
713
1102
|
|
|
714
1103
|
// 2) Fetch ids for batching
|
|
715
|
-
const seeded = await menuRepo.find({
|
|
1104
|
+
const seeded = await this.timeOperation('menu-seeded-find', () => menuRepo.find({
|
|
716
1105
|
where: { name: In(menus.map((m: any) => m.name)) },
|
|
717
1106
|
select: ["id", "name"],
|
|
1107
|
+
}), {
|
|
1108
|
+
component: 'menus',
|
|
1109
|
+
serviceCall: 'menuRepo.find',
|
|
1110
|
+
details: `count=${menus.length}`,
|
|
718
1111
|
});
|
|
719
1112
|
const idByName = new Map(seeded.map(s => [s.name, s.id]));
|
|
720
1113
|
|
|
721
1114
|
// 3) Build desired join rows once
|
|
722
|
-
const admin = await roleRepo.findOne({ where: { name: ADMIN_ROLE_NAME }, select: ["id", "name"] })
|
|
1115
|
+
const admin = await this.timeOperation('menu-admin-role-find-one', () => roleRepo.findOne({ where: { name: ADMIN_ROLE_NAME }, select: ["id", "name"] }), {
|
|
1116
|
+
component: 'menus',
|
|
1117
|
+
serviceCall: 'roleRepo.findOne',
|
|
1118
|
+
details: `role=${ADMIN_ROLE_NAME}`,
|
|
1119
|
+
});
|
|
723
1120
|
const allRoleNames = new Set<string>();
|
|
724
1121
|
for (const m of menus) (m.roles ?? []).forEach((r: string) => allRoleNames.add(r));
|
|
725
1122
|
if (admin) allRoleNames.add(admin.name);
|
|
726
1123
|
|
|
727
|
-
const roles = await roleRepo.find({
|
|
1124
|
+
const roles = await this.timeOperation('menu-roles-find', () => roleRepo.find({
|
|
728
1125
|
where: { name: In([...allRoleNames]) },
|
|
729
1126
|
select: ["id", "name"],
|
|
1127
|
+
}), {
|
|
1128
|
+
component: 'menus',
|
|
1129
|
+
serviceCall: 'roleRepo.find',
|
|
1130
|
+
details: `roleCount=${allRoleNames.size}`,
|
|
730
1131
|
});
|
|
731
1132
|
const roleByName = new Map(roles.map(r => [r.name, r]));
|
|
732
1133
|
|
|
@@ -744,12 +1145,16 @@ export class ModuleMetadataSeederService {
|
|
|
744
1145
|
// 4a) delete existing for affected menus
|
|
745
1146
|
const menuIds = [...new Set(joinRows.map(r => r.menuId))];
|
|
746
1147
|
if (menuIds.length) {
|
|
747
|
-
await trx
|
|
1148
|
+
await this.timeOperation('menu-role-joins-delete', () => trx
|
|
748
1149
|
.createQueryBuilder()
|
|
749
1150
|
.delete()
|
|
750
1151
|
.from(MENU_ROLE_JOIN_TABLE_NAME) // string table name is fine
|
|
751
1152
|
.where(`${MENU_ROLE_JOIN_TABLE_NAME_MENU_COL} IN (:...ids)`, { ids: menuIds })
|
|
752
|
-
.execute()
|
|
1153
|
+
.execute(), {
|
|
1154
|
+
component: 'menus',
|
|
1155
|
+
serviceCall: 'trx.delete.menuRoleJoins',
|
|
1156
|
+
details: `menuCount=${menuIds.length}`,
|
|
1157
|
+
});
|
|
753
1158
|
}
|
|
754
1159
|
|
|
755
1160
|
// 4b) bulk insert all pairs
|
|
@@ -759,15 +1164,19 @@ export class ModuleMetadataSeederService {
|
|
|
759
1164
|
[MENU_ROLE_JOIN_TABLE_NAME_ROLE_COL]: r.roleId,
|
|
760
1165
|
}));
|
|
761
1166
|
|
|
762
|
-
await trx
|
|
1167
|
+
await this.timeOperation('menu-role-joins-insert', () => trx
|
|
763
1168
|
.createQueryBuilder()
|
|
764
1169
|
.insert()
|
|
765
1170
|
.into(MENU_ROLE_JOIN_TABLE_NAME)
|
|
766
1171
|
.values(values)
|
|
767
1172
|
// .orIgnore() // ❌ remove this – it triggers unsupported onUpdate path
|
|
768
|
-
.execute()
|
|
1173
|
+
.execute(), {
|
|
1174
|
+
component: 'menus',
|
|
1175
|
+
serviceCall: 'trx.insert.menuRoleJoins',
|
|
1176
|
+
details: `rowCount=${values.length}`,
|
|
1177
|
+
});
|
|
769
1178
|
}
|
|
770
|
-
});
|
|
1179
|
+
}), { component: 'menus', serviceCall: 'dataSource.transaction' });
|
|
771
1180
|
}
|
|
772
1181
|
|
|
773
1182
|
// Ok
|
|
@@ -776,20 +1185,81 @@ export class ModuleMetadataSeederService {
|
|
|
776
1185
|
return;
|
|
777
1186
|
}
|
|
778
1187
|
|
|
1188
|
+
const actionRepo = this.dataSource.getRepository(ActionMetadata);
|
|
1189
|
+
// Prime related-entity caches once before iterating so we do not pay repeated user-key lookups per row.
|
|
1190
|
+
await this.timeOperation('action-prime-module-cache', () => this.primeModuleLookupCache(
|
|
1191
|
+
actions.map((action: any) => action?.moduleUserKey),
|
|
1192
|
+
), {
|
|
1193
|
+
component: 'actions',
|
|
1194
|
+
serviceCall: 'primeModuleLookupCache',
|
|
1195
|
+
details: `count=${actions.length}`,
|
|
1196
|
+
});
|
|
1197
|
+
await this.timeOperation('action-prime-model-cache', () => this.primeModelLookupCache(
|
|
1198
|
+
actions.map((action: any) => action?.modelUserKey),
|
|
1199
|
+
), {
|
|
1200
|
+
component: 'actions',
|
|
1201
|
+
serviceCall: 'primeModelLookupCache',
|
|
1202
|
+
details: `count=${actions.length}`,
|
|
1203
|
+
});
|
|
1204
|
+
await this.timeOperation('action-prime-view-cache', () => this.primeViewLookupCache(
|
|
1205
|
+
actions.map((action: any) => action?.viewUserKey),
|
|
1206
|
+
), {
|
|
1207
|
+
component: 'actions',
|
|
1208
|
+
serviceCall: 'primeViewLookupCache',
|
|
1209
|
+
details: `count=${actions.length}`,
|
|
1210
|
+
});
|
|
1211
|
+
const existingActionsByName = await this.timeOperation('action-preload-existing', () => this.loadExistingActionsByName(
|
|
1212
|
+
actionRepo,
|
|
1213
|
+
actions.map((action: any) => action?.name).filter(Boolean),
|
|
1214
|
+
), {
|
|
1215
|
+
component: 'actions',
|
|
1216
|
+
serviceCall: 'loadExistingActionsByName',
|
|
1217
|
+
details: `count=${actions.length}`,
|
|
1218
|
+
});
|
|
1219
|
+
// Keep the verbose output summarized rather than emitting forty separate "skip" lines.
|
|
1220
|
+
let created = 0;
|
|
1221
|
+
let updated = 0;
|
|
1222
|
+
let skipped = 0;
|
|
1223
|
+
|
|
779
1224
|
for (let j = 0; j < actions.length; j++) {
|
|
780
1225
|
const actionData = actions[j];
|
|
781
|
-
actionData['module'] = await this.
|
|
1226
|
+
actionData['module'] = await this.getModuleByUserKeyCached(actionData.moduleUserKey, {
|
|
1227
|
+
moduleName: actionData.moduleUserKey,
|
|
1228
|
+
component: 'actions',
|
|
1229
|
+
details: `module=${actionData.moduleUserKey}`,
|
|
1230
|
+
});
|
|
782
1231
|
if (actionData.type === 'solid') {
|
|
783
|
-
actionData['model'] = await this.
|
|
784
|
-
|
|
1232
|
+
actionData['model'] = await this.getModelByUserKeyCached(actionData.modelUserKey, {
|
|
1233
|
+
moduleName: actionData.moduleUserKey,
|
|
1234
|
+
component: 'actions',
|
|
1235
|
+
details: `model=${actionData.modelUserKey}`,
|
|
1236
|
+
});
|
|
1237
|
+
actionData['view'] = await this.getViewByUserKeyCached(actionData.viewUserKey, {
|
|
1238
|
+
moduleName: actionData.moduleUserKey,
|
|
1239
|
+
component: 'actions',
|
|
1240
|
+
details: `view=${actionData.viewUserKey}`,
|
|
1241
|
+
});
|
|
785
1242
|
}
|
|
786
1243
|
else {
|
|
787
1244
|
if (actionData.modelUserKey) {
|
|
788
|
-
actionData['model'] = await this.
|
|
1245
|
+
actionData['model'] = await this.getModelByUserKeyCached(actionData.modelUserKey, {
|
|
1246
|
+
moduleName: actionData.moduleUserKey,
|
|
1247
|
+
component: 'actions',
|
|
1248
|
+
details: `model=${actionData.modelUserKey}`,
|
|
1249
|
+
});
|
|
789
1250
|
}
|
|
790
1251
|
}
|
|
791
|
-
await this.
|
|
1252
|
+
const result = await this.upsertActionMetadataWithPreloadedExisting(actionRepo, existingActionsByName, actionData);
|
|
1253
|
+
if (result.outcome === 'created') {
|
|
1254
|
+
created += 1;
|
|
1255
|
+
} else if (result.outcome === 'updated') {
|
|
1256
|
+
updated += 1;
|
|
1257
|
+
} else {
|
|
1258
|
+
skipped += 1;
|
|
1259
|
+
}
|
|
792
1260
|
}
|
|
1261
|
+
|
|
1262
|
+
this.logger.debug(`Action seed summary: created=${created}, updated=${updated}, skipped=${skipped}.`);
|
|
793
1263
|
}
|
|
794
1264
|
|
|
795
1265
|
// Ok
|
|
@@ -798,18 +1268,64 @@ export class ModuleMetadataSeederService {
|
|
|
798
1268
|
return;
|
|
799
1269
|
}
|
|
800
1270
|
|
|
1271
|
+
const viewRepo = this.dataSource.getRepository(ViewMetadata);
|
|
1272
|
+
// Same preload/diff strategy as actions: resolve relation references in bulk, load existing rows once,
|
|
1273
|
+
// then do the rest of the work in memory so unchanged views never fall through to save().
|
|
1274
|
+
await this.timeOperation('view-prime-module-cache', () => this.primeModuleLookupCache(
|
|
1275
|
+
views.map((view: any) => view?.moduleUserKey),
|
|
1276
|
+
), {
|
|
1277
|
+
component: 'views',
|
|
1278
|
+
serviceCall: 'primeModuleLookupCache',
|
|
1279
|
+
details: `count=${views.length}`,
|
|
1280
|
+
});
|
|
1281
|
+
await this.timeOperation('view-prime-model-cache', () => this.primeModelLookupCache(
|
|
1282
|
+
views.map((view: any) => view?.modelUserKey),
|
|
1283
|
+
), {
|
|
1284
|
+
component: 'views',
|
|
1285
|
+
serviceCall: 'primeModelLookupCache',
|
|
1286
|
+
details: `count=${views.length}`,
|
|
1287
|
+
});
|
|
1288
|
+
const existingViewsByName = await this.timeOperation('view-preload-existing', () => this.loadExistingViewsByName(
|
|
1289
|
+
viewRepo,
|
|
1290
|
+
views.map((view: any) => view?.name).filter(Boolean),
|
|
1291
|
+
), {
|
|
1292
|
+
component: 'views',
|
|
1293
|
+
serviceCall: 'loadExistingViewsByName',
|
|
1294
|
+
details: `count=${views.length}`,
|
|
1295
|
+
});
|
|
1296
|
+
let created = 0;
|
|
1297
|
+
let updated = 0;
|
|
1298
|
+
let skipped = 0;
|
|
1299
|
+
|
|
801
1300
|
for (let j = 0; j < views.length; j++) {
|
|
802
1301
|
const viewData = views[j];
|
|
803
1302
|
|
|
804
1303
|
// preety format the layout & context.
|
|
805
1304
|
viewData['layout'] = JSON.stringify(viewData['layout'], null, 2);
|
|
806
1305
|
|
|
807
|
-
viewData['module'] = await this.
|
|
808
|
-
|
|
1306
|
+
viewData['module'] = await this.getModuleByUserKeyCached(viewData.moduleUserKey, {
|
|
1307
|
+
moduleName: viewData.moduleUserKey,
|
|
1308
|
+
component: 'views',
|
|
1309
|
+
details: `module=${viewData.moduleUserKey}`,
|
|
1310
|
+
});
|
|
1311
|
+
viewData['model'] = await this.getModelByUserKeyCached(viewData.modelUserKey, {
|
|
1312
|
+
moduleName: viewData.moduleUserKey,
|
|
1313
|
+
component: 'views',
|
|
1314
|
+
details: `model=${viewData.modelUserKey}`,
|
|
1315
|
+
});
|
|
809
1316
|
|
|
810
|
-
|
|
811
|
-
|
|
1317
|
+
const result = await this.upsertViewMetadataWithPreloadedExisting(viewRepo, existingViewsByName, viewData);
|
|
1318
|
+
this.viewLookupCache.set(viewData.name, result.entity ?? null);
|
|
1319
|
+
if (result.outcome === 'created') {
|
|
1320
|
+
created += 1;
|
|
1321
|
+
} else if (result.outcome === 'updated') {
|
|
1322
|
+
updated += 1;
|
|
1323
|
+
} else {
|
|
1324
|
+
skipped += 1;
|
|
1325
|
+
}
|
|
812
1326
|
}
|
|
1327
|
+
|
|
1328
|
+
this.logger.debug(`View seed summary: created=${created}, updated=${updated}, skipped=${skipped}.`);
|
|
813
1329
|
}
|
|
814
1330
|
|
|
815
1331
|
// OK
|
|
@@ -820,13 +1336,21 @@ export class ModuleMetadataSeederService {
|
|
|
820
1336
|
|
|
821
1337
|
for (let l = 0; l < users.length; l++) {
|
|
822
1338
|
const user: SignUpDto = users[l];
|
|
823
|
-
let exisitingUser = await this.userService.findOneByUsername(user.username)
|
|
1339
|
+
let exisitingUser = await this.timeOperation('user-find-by-username', () => this.userService.findOneByUsername(user.username), {
|
|
1340
|
+
component: 'users',
|
|
1341
|
+
serviceCall: 'userService.findOneByUsername',
|
|
1342
|
+
details: `username=${user.username}`,
|
|
1343
|
+
});
|
|
824
1344
|
if (!exisitingUser) {
|
|
825
1345
|
if (user.username === 'sa') {
|
|
826
1346
|
user.password = DEFAULT_SA_PASSWORD;
|
|
827
1347
|
}
|
|
828
1348
|
|
|
829
|
-
exisitingUser = await this.authenticationService.signUp(user)
|
|
1349
|
+
exisitingUser = await this.timeOperation('user-sign-up', () => this.authenticationService.signUp(user), {
|
|
1350
|
+
component: 'users',
|
|
1351
|
+
serviceCall: 'authenticationService.signUp',
|
|
1352
|
+
details: `username=${user.username}`,
|
|
1353
|
+
});
|
|
830
1354
|
this.logger.log(`Newly created user ${user.username}`);
|
|
831
1355
|
}
|
|
832
1356
|
//FIXME: Create the user roles assignment logic here.
|
|
@@ -840,10 +1364,16 @@ export class ModuleMetadataSeederService {
|
|
|
840
1364
|
|
|
841
1365
|
// OK
|
|
842
1366
|
private async seedModuleModelFields(moduleMetadata: CreateModuleMetadataDto): Promise<{ pruned: number; upserted: number }> {
|
|
843
|
-
this.
|
|
1367
|
+
const fieldMetadataRepo = this.dataSource.getRepository(FieldMetadata);
|
|
844
1368
|
|
|
845
1369
|
// First we create the module.
|
|
846
|
-
const module = await this.moduleMetadataService.upsert(moduleMetadata)
|
|
1370
|
+
const module = await this.timeOperation('module-upsert', () => this.moduleMetadataService.upsert(moduleMetadata), {
|
|
1371
|
+
moduleName: moduleMetadata.name,
|
|
1372
|
+
component: 'module-model-fields',
|
|
1373
|
+
serviceCall: 'moduleMetadataService.upsert',
|
|
1374
|
+
details: `module=${moduleMetadata.name}`,
|
|
1375
|
+
});
|
|
1376
|
+
this.moduleLookupCache.set(moduleMetadata.name, module ?? null);
|
|
847
1377
|
let pruned = 0;
|
|
848
1378
|
let upserted = 1;
|
|
849
1379
|
|
|
@@ -859,19 +1389,47 @@ export class ModuleMetadataSeederService {
|
|
|
859
1389
|
|
|
860
1390
|
// Load and set the parent model if it exists.
|
|
861
1391
|
if (modelMetadata.isChild && modelMetadata.parentModelUserKey) {
|
|
862
|
-
const parentModel = await this.
|
|
1392
|
+
const parentModel = await this.getModelByUserKeyCached(modelMetadata.parentModelUserKey, {
|
|
1393
|
+
moduleName: moduleMetadata.name,
|
|
1394
|
+
component: 'module-model-fields',
|
|
1395
|
+
details: `parentModel=${modelMetadata.parentModelUserKey}`,
|
|
1396
|
+
});
|
|
863
1397
|
modelMetaDataWithoutFields['parentModel'] = parentModel;
|
|
864
1398
|
}
|
|
865
1399
|
|
|
866
|
-
await this.modelMetadataService.upsert(modelMetaDataWithoutFields)
|
|
867
|
-
|
|
1400
|
+
const upsertedModel = await this.timeOperation('model-upsert', () => this.modelMetadataService.upsert(modelMetaDataWithoutFields), {
|
|
1401
|
+
moduleName: moduleMetadata.name,
|
|
1402
|
+
component: 'module-model-fields',
|
|
1403
|
+
serviceCall: 'modelMetadataService.upsert',
|
|
1404
|
+
details: `model=${modelMetadata.singularName}`,
|
|
1405
|
+
});
|
|
1406
|
+
this.modelLookupCache.set(modelMetadata.singularName, upsertedModel ?? null);
|
|
1407
|
+
const model = await this.timeOperation('model-find-by-singular-name', () => this.modelMetadataService.findOneBySingularName(modelMetadata.singularName), {
|
|
1408
|
+
moduleName: moduleMetadata.name,
|
|
1409
|
+
component: 'module-model-fields',
|
|
1410
|
+
serviceCall: 'modelMetadataService.findOneBySingularName',
|
|
1411
|
+
details: `model=${modelMetadata.singularName}`,
|
|
1412
|
+
});
|
|
868
1413
|
|
|
869
1414
|
// iterate over all fields and upsert.
|
|
870
1415
|
if (this.enablePruning) {
|
|
871
|
-
pruned += await this.pruneFieldsForModel(model, fieldsMetadata)
|
|
1416
|
+
pruned += await this.timeOperation('prune-fields-for-model', () => this.pruneFieldsForModel(model, fieldsMetadata), {
|
|
1417
|
+
moduleName: moduleMetadata.name,
|
|
1418
|
+
component: 'module-model-fields',
|
|
1419
|
+
serviceCall: 'pruneFieldsForModel',
|
|
1420
|
+
details: `model=${modelMetadata.singularName}`,
|
|
1421
|
+
});
|
|
872
1422
|
}
|
|
873
1423
|
let userKeyField = null;
|
|
874
1424
|
const userKeyFieldName = modelMetadata.userKeyFieldUserKey;
|
|
1425
|
+
// Preload all existing fields for the model once. This is why field upserts are now effectively cheap:
|
|
1426
|
+
// we no longer do row-by-row existence checks for every field in steady-state runs.
|
|
1427
|
+
const existingFieldsByName = await this.timeOperation('field-preload-for-model', () => this.loadExistingFieldsByName(fieldMetadataRepo, model.id), {
|
|
1428
|
+
moduleName: moduleMetadata.name,
|
|
1429
|
+
component: 'module-model-fields',
|
|
1430
|
+
serviceCall: 'loadExistingFieldsByName',
|
|
1431
|
+
details: `model=${modelMetadata.singularName}`,
|
|
1432
|
+
});
|
|
875
1433
|
upserted += fieldsMetadata?.length ?? 0;
|
|
876
1434
|
for (let k = 0; k < fieldsMetadata.length; k++) {
|
|
877
1435
|
const fieldMetadata = fieldsMetadata[k];
|
|
@@ -879,11 +1437,20 @@ export class ModuleMetadataSeederService {
|
|
|
879
1437
|
// Link model & mediaStorageProvider.
|
|
880
1438
|
fieldMetadata['model'] = model;
|
|
881
1439
|
if (fieldMetadata.mediaStorageProviderUserKey) {
|
|
882
|
-
fieldMetadata['mediaStorageProvider'] = await this.
|
|
1440
|
+
fieldMetadata['mediaStorageProvider'] = await this.getMediaStorageProviderByUserKeyCached(fieldMetadata.mediaStorageProviderUserKey, {
|
|
1441
|
+
moduleName: moduleMetadata.name,
|
|
1442
|
+
component: 'module-model-fields',
|
|
1443
|
+
details: `provider=${fieldMetadata.mediaStorageProviderUserKey}`,
|
|
1444
|
+
});
|
|
883
1445
|
}
|
|
884
1446
|
// console.log(fieldMetadata.displayName);
|
|
885
1447
|
|
|
886
|
-
const affectedField = await this.
|
|
1448
|
+
const affectedField = await this.timeOperation('field-upsert', () => this.upsertFieldMetadataWithPreloadedExisting(fieldMetadataRepo, existingFieldsByName, fieldMetadata), {
|
|
1449
|
+
moduleName: moduleMetadata.name,
|
|
1450
|
+
component: 'module-model-fields',
|
|
1451
|
+
serviceCall: 'upsertFieldMetadataWithPreloadedExisting',
|
|
1452
|
+
details: `field=${fieldMetadata.name} model=${modelMetadata.singularName}`,
|
|
1453
|
+
});
|
|
887
1454
|
if (fieldMetadata.name === userKeyFieldName || fieldMetadata.isUserKey) {
|
|
888
1455
|
const { model, ...fieldData } = affectedField;
|
|
889
1456
|
userKeyField = fieldData;
|
|
@@ -893,13 +1460,21 @@ export class ModuleMetadataSeederService {
|
|
|
893
1460
|
// Now that we have created fields & model update the model to stamp the userKeyField.
|
|
894
1461
|
if (userKeyField) {
|
|
895
1462
|
modelMetaDataWithoutFields['userKeyField'] = userKeyField;
|
|
896
|
-
await this.modelMetadataService.upsert(modelMetaDataWithoutFields)
|
|
1463
|
+
await this.timeOperation('model-user-key-field-upsert', () => this.modelMetadataService.upsert(modelMetaDataWithoutFields), {
|
|
1464
|
+
moduleName: moduleMetadata.name,
|
|
1465
|
+
component: 'module-model-fields',
|
|
1466
|
+
serviceCall: 'modelMetadataService.upsert',
|
|
1467
|
+
details: `model=${modelMetadata.singularName} userKeyField=${userKeyField?.name ?? 'unknown'}`,
|
|
1468
|
+
});
|
|
897
1469
|
}
|
|
898
1470
|
}
|
|
899
1471
|
if (this.enablePruning) {
|
|
900
|
-
pruned += await this.pruneModels(modelsMetadata, moduleMetadata.name)
|
|
1472
|
+
pruned += await this.timeOperation('prune-models', () => this.pruneModels(modelsMetadata, moduleMetadata.name), {
|
|
1473
|
+
moduleName: moduleMetadata.name,
|
|
1474
|
+
component: 'module-model-fields',
|
|
1475
|
+
serviceCall: 'pruneModels',
|
|
1476
|
+
});
|
|
901
1477
|
}
|
|
902
|
-
this.logger.debug(`[End] Processing module metadata`);
|
|
903
1478
|
return { pruned, upserted };
|
|
904
1479
|
}
|
|
905
1480
|
|
|
@@ -907,13 +1482,343 @@ export class ModuleMetadataSeederService {
|
|
|
907
1482
|
return Array.isArray(value) ? value : [];
|
|
908
1483
|
}
|
|
909
1484
|
|
|
1485
|
+
private async loadExistingFieldsByName(fieldMetadataRepo: Repository<FieldMetadata>, modelId: number): Promise<Map<string, FieldMetadata>> {
|
|
1486
|
+
const existingFields = await fieldMetadataRepo.find({
|
|
1487
|
+
where: {
|
|
1488
|
+
model: { id: modelId },
|
|
1489
|
+
},
|
|
1490
|
+
relations: {
|
|
1491
|
+
model: true,
|
|
1492
|
+
mediaStorageProvider: true,
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
return new Map(existingFields.map((field) => [field.name, field]));
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
private async primeModuleLookupCache(moduleUserKeys: Array<string | null | undefined>): Promise<void> {
|
|
1500
|
+
const missingModuleUserKeys = [...new Set(moduleUserKeys.filter((moduleUserKey): moduleUserKey is string =>
|
|
1501
|
+
Boolean(moduleUserKey) && !this.moduleLookupCache.has(moduleUserKey),
|
|
1502
|
+
))];
|
|
1503
|
+
|
|
1504
|
+
if (missingModuleUserKeys.length === 0) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Only bulk-load keys that are still missing from the process-local cache.
|
|
1509
|
+
const moduleRepo = this.dataSource.getRepository(ModuleMetadata);
|
|
1510
|
+
const existingModules = await moduleRepo.find({
|
|
1511
|
+
where: {
|
|
1512
|
+
name: In(missingModuleUserKeys),
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
const moduleByName = new Map(existingModules.map((module) => [module.name, module]));
|
|
1517
|
+
for (const moduleUserKey of missingModuleUserKeys) {
|
|
1518
|
+
this.moduleLookupCache.set(moduleUserKey, moduleByName.get(moduleUserKey) ?? null);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
private async primeModelLookupCache(modelUserKeys: Array<string | null | undefined>): Promise<void> {
|
|
1523
|
+
const missingModelUserKeys = [...new Set(modelUserKeys.filter((modelUserKey): modelUserKey is string =>
|
|
1524
|
+
Boolean(modelUserKey) && !this.modelLookupCache.has(modelUserKey),
|
|
1525
|
+
))];
|
|
1526
|
+
|
|
1527
|
+
if (missingModelUserKeys.length === 0) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const modelRepo = this.dataSource.getRepository(ModelMetadata);
|
|
1532
|
+
const existingModels = await modelRepo.find({
|
|
1533
|
+
where: {
|
|
1534
|
+
singularName: In(missingModelUserKeys),
|
|
1535
|
+
},
|
|
1536
|
+
relations: {
|
|
1537
|
+
module: true,
|
|
1538
|
+
userKeyField: true,
|
|
1539
|
+
},
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
const modelBySingularName = new Map(existingModels.map((model) => [model.singularName, model]));
|
|
1543
|
+
for (const modelUserKey of missingModelUserKeys) {
|
|
1544
|
+
this.modelLookupCache.set(modelUserKey, modelBySingularName.get(modelUserKey) ?? null);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
private async primeViewLookupCache(viewUserKeys: Array<string | null | undefined>): Promise<void> {
|
|
1549
|
+
const missingViewUserKeys = [...new Set(viewUserKeys.filter((viewUserKey): viewUserKey is string =>
|
|
1550
|
+
Boolean(viewUserKey) && !this.viewLookupCache.has(viewUserKey),
|
|
1551
|
+
))];
|
|
1552
|
+
|
|
1553
|
+
if (missingViewUserKeys.length === 0) {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const viewRepo = this.dataSource.getRepository(ViewMetadata);
|
|
1558
|
+
const existingViews = await viewRepo.find({
|
|
1559
|
+
where: {
|
|
1560
|
+
name: In(missingViewUserKeys),
|
|
1561
|
+
},
|
|
1562
|
+
relations: {
|
|
1563
|
+
module: true,
|
|
1564
|
+
model: true,
|
|
1565
|
+
},
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
const viewByName = new Map(existingViews.map((view) => [view.name, view]));
|
|
1569
|
+
for (const viewUserKey of missingViewUserKeys) {
|
|
1570
|
+
this.viewLookupCache.set(viewUserKey, viewByName.get(viewUserKey) ?? null);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
private async loadExistingViewsByName(viewRepo: Repository<ViewMetadata>, viewNames: string[]): Promise<Map<string, ViewMetadata>> {
|
|
1575
|
+
const uniqueViewNames = [...new Set(viewNames.filter(Boolean))];
|
|
1576
|
+
if (uniqueViewNames.length === 0) {
|
|
1577
|
+
return new Map();
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// Load relation ids needed for no-op detection. If module/model are not present here, unchanged rows look
|
|
1581
|
+
// dirty and every view falls through to save().
|
|
1582
|
+
const existingViews = await viewRepo.find({
|
|
1583
|
+
where: {
|
|
1584
|
+
name: In(uniqueViewNames),
|
|
1585
|
+
},
|
|
1586
|
+
relations: {
|
|
1587
|
+
module: true,
|
|
1588
|
+
model: true,
|
|
1589
|
+
},
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
return new Map(existingViews.map((view) => [view.name, view]));
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
private async loadExistingActionsByName(actionRepo: Repository<ActionMetadata>, actionNames: string[]): Promise<Map<string, ActionMetadata>> {
|
|
1596
|
+
const uniqueActionNames = [...new Set(actionNames.filter(Boolean))];
|
|
1597
|
+
if (uniqueActionNames.length === 0) {
|
|
1598
|
+
return new Map();
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Same reason as views: the diff logic compares relation ids, so those relations must be loaded here.
|
|
1602
|
+
const existingActions = await actionRepo.find({
|
|
1603
|
+
where: {
|
|
1604
|
+
name: In(uniqueActionNames),
|
|
1605
|
+
},
|
|
1606
|
+
relations: {
|
|
1607
|
+
module: true,
|
|
1608
|
+
model: true,
|
|
1609
|
+
view: true,
|
|
1610
|
+
},
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
return new Map(existingActions.map((action) => [action.name, action]));
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
private normalizeJsonFieldValue(value: any): any {
|
|
1617
|
+
// Normalize persisted JSON strings and in-memory objects/arrays into the same canonical shape before compare.
|
|
1618
|
+
if (typeof value === 'string') {
|
|
1619
|
+
const trimmedValue = value.trim();
|
|
1620
|
+
if (
|
|
1621
|
+
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}'))
|
|
1622
|
+
|| (trimmedValue.startsWith('[') && trimmedValue.endsWith(']'))
|
|
1623
|
+
) {
|
|
1624
|
+
try {
|
|
1625
|
+
return this.normalizeJsonFieldValue(JSON.parse(trimmedValue));
|
|
1626
|
+
} catch {
|
|
1627
|
+
return value;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
return value;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
if (Array.isArray(value)) {
|
|
1635
|
+
return value.map((item) => this.normalizeJsonFieldValue(item));
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (value && typeof value === 'object') {
|
|
1639
|
+
return Object.keys(value)
|
|
1640
|
+
.sort()
|
|
1641
|
+
.reduce((result, key) => {
|
|
1642
|
+
result[key] = this.normalizeJsonFieldValue(value[key]);
|
|
1643
|
+
return result;
|
|
1644
|
+
}, {} as Record<string, any>);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
return value;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
private getCanonicalJsonFieldString(value: any): string {
|
|
1651
|
+
return JSON.stringify(this.normalizeJsonFieldValue(value) ?? null);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
private hasViewMetadataChanges(existingView: ViewMetadata, updateDto: any): boolean {
|
|
1655
|
+
const relationFields = new Set(['module', 'model']);
|
|
1656
|
+
const jsonFields = new Set(['layout', 'context']);
|
|
1657
|
+
|
|
1658
|
+
// Compare only the persisted fields we explicitly care about. Generic object-wide diffing became fragile once
|
|
1659
|
+
// seeding DTOs started carrying transient helper properties that should not trigger writes.
|
|
1660
|
+
return this.viewComparableFields.some((key) => {
|
|
1661
|
+
const value = updateDto[key];
|
|
1662
|
+
if (typeof value === 'undefined') {
|
|
1663
|
+
return false;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (relationFields.has(key)) {
|
|
1667
|
+
const relationValue = value as any;
|
|
1668
|
+
return (existingView as any)[key]?.id !== relationValue?.id;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
if (jsonFields.has(key)) {
|
|
1672
|
+
return this.getCanonicalJsonFieldString((existingView as any)[key]) !== this.getCanonicalJsonFieldString(value);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
return (existingView as any)[key] !== value;
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
private hasActionMetadataChanges(existingAction: ActionMetadata, updateDto: any): boolean {
|
|
1680
|
+
const relationFields = new Set(['module', 'model', 'view']);
|
|
1681
|
+
const jsonFields = new Set(['domain', 'context']);
|
|
1682
|
+
|
|
1683
|
+
return this.actionComparableFields.some((key) => {
|
|
1684
|
+
const value = updateDto[key];
|
|
1685
|
+
if (typeof value === 'undefined') {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (relationFields.has(key)) {
|
|
1690
|
+
const relationValue = value as any;
|
|
1691
|
+
return (existingAction as any)[key]?.id !== relationValue?.id;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (jsonFields.has(key)) {
|
|
1695
|
+
return this.getCanonicalJsonFieldString((existingAction as any)[key]) !== this.getCanonicalJsonFieldString(value);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
return (existingAction as any)[key] !== value;
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
private async upsertViewMetadataWithPreloadedExisting(
|
|
1703
|
+
viewRepo: Repository<ViewMetadata>,
|
|
1704
|
+
existingViewsByName: Map<string, ViewMetadata>,
|
|
1705
|
+
updateDto: any,
|
|
1706
|
+
): Promise<{ entity: ViewMetadata; outcome: 'created' | 'updated' | 'skipped' }> {
|
|
1707
|
+
const existingView = existingViewsByName.get(updateDto.name);
|
|
1708
|
+
|
|
1709
|
+
if (existingView) {
|
|
1710
|
+
// In the steady-state case we want to return fast here and avoid save() entirely.
|
|
1711
|
+
if (!this.hasViewMetadataChanges(existingView, updateDto)) {
|
|
1712
|
+
return { entity: existingView, outcome: 'skipped' };
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const updatedView = { ...existingView, ...updateDto };
|
|
1716
|
+
const savedView = await this.timeOperation('view-save', () => viewRepo.save(updatedView as ViewMetadata), {
|
|
1717
|
+
component: 'views',
|
|
1718
|
+
serviceCall: 'viewRepo.save',
|
|
1719
|
+
details: `view=${updateDto.name}`,
|
|
1720
|
+
}) as ViewMetadata;
|
|
1721
|
+
existingViewsByName.set(updateDto.name, savedView);
|
|
1722
|
+
return { entity: savedView, outcome: 'updated' };
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const view = viewRepo.create(updateDto as Partial<ViewMetadata>);
|
|
1726
|
+
const savedView = await this.timeOperation('view-save', () => viewRepo.save(view as ViewMetadata), {
|
|
1727
|
+
component: 'views',
|
|
1728
|
+
serviceCall: 'viewRepo.save',
|
|
1729
|
+
details: `view=${updateDto.name}`,
|
|
1730
|
+
}) as ViewMetadata;
|
|
1731
|
+
existingViewsByName.set(updateDto.name, savedView);
|
|
1732
|
+
return { entity: savedView, outcome: 'created' };
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
private async upsertActionMetadataWithPreloadedExisting(
|
|
1736
|
+
actionRepo: Repository<ActionMetadata>,
|
|
1737
|
+
existingActionsByName: Map<string, ActionMetadata>,
|
|
1738
|
+
updateDto: any,
|
|
1739
|
+
): Promise<{ entity: ActionMetadata; outcome: 'created' | 'updated' | 'skipped' }> {
|
|
1740
|
+
const existingAction = existingActionsByName.get(updateDto.name);
|
|
1741
|
+
|
|
1742
|
+
if (existingAction) {
|
|
1743
|
+
if (!this.hasActionMetadataChanges(existingAction, updateDto)) {
|
|
1744
|
+
return { entity: existingAction, outcome: 'skipped' };
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
const updatedAction = { ...existingAction, ...updateDto };
|
|
1748
|
+
const savedAction = await this.timeOperation('action-save', () => actionRepo.save(updatedAction as ActionMetadata), {
|
|
1749
|
+
component: 'actions',
|
|
1750
|
+
serviceCall: 'actionRepo.save',
|
|
1751
|
+
details: `action=${updateDto.name}`,
|
|
1752
|
+
}) as ActionMetadata;
|
|
1753
|
+
existingActionsByName.set(updateDto.name, savedAction);
|
|
1754
|
+
return { entity: savedAction, outcome: 'updated' };
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const action = actionRepo.create(updateDto as Partial<ActionMetadata>);
|
|
1758
|
+
const savedAction = await this.timeOperation('action-save', () => actionRepo.save(action as ActionMetadata), {
|
|
1759
|
+
component: 'actions',
|
|
1760
|
+
serviceCall: 'actionRepo.save',
|
|
1761
|
+
details: `action=${updateDto.name}`,
|
|
1762
|
+
}) as ActionMetadata;
|
|
1763
|
+
existingActionsByName.set(updateDto.name, savedAction);
|
|
1764
|
+
return { entity: savedAction, outcome: 'created' };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
private async upsertFieldMetadataWithPreloadedExisting(
|
|
1768
|
+
fieldMetadataRepo: Repository<FieldMetadata>,
|
|
1769
|
+
existingFieldsByName: Map<string, FieldMetadata>,
|
|
1770
|
+
updateDto: any,
|
|
1771
|
+
): Promise<FieldMetadata> {
|
|
1772
|
+
const existingFieldMetadata = existingFieldsByName.get(updateDto.name);
|
|
1773
|
+
|
|
1774
|
+
if (existingFieldMetadata) {
|
|
1775
|
+
// Field metadata still uses a broad DTO diff because the major performance problem here was query volume,
|
|
1776
|
+
// not the in-memory comparison itself.
|
|
1777
|
+
const hasChanges = Object.entries(updateDto).some(([key, value]) => {
|
|
1778
|
+
const relationValue = value as any;
|
|
1779
|
+
if (key === 'model') {
|
|
1780
|
+
return existingFieldMetadata.model?.id !== relationValue?.id;
|
|
1781
|
+
}
|
|
1782
|
+
if (key === 'mediaStorageProvider') {
|
|
1783
|
+
return existingFieldMetadata.mediaStorageProvider?.id !== relationValue?.id;
|
|
1784
|
+
}
|
|
1785
|
+
const currentValue = (existingFieldMetadata as any)[key];
|
|
1786
|
+
if (Array.isArray(currentValue) || Array.isArray(value)) {
|
|
1787
|
+
return JSON.stringify(currentValue ?? null) !== JSON.stringify(value ?? null);
|
|
1788
|
+
}
|
|
1789
|
+
if (value && typeof value === 'object') {
|
|
1790
|
+
return JSON.stringify(currentValue ?? null) !== JSON.stringify(value ?? null);
|
|
1791
|
+
}
|
|
1792
|
+
return currentValue !== value;
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
if (!hasChanges) {
|
|
1796
|
+
return existingFieldMetadata;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const updatedFieldMetadata = { ...existingFieldMetadata, ...updateDto };
|
|
1800
|
+
const savedFieldMetadata = await fieldMetadataRepo.save(updatedFieldMetadata as FieldMetadata) as FieldMetadata;
|
|
1801
|
+
existingFieldsByName.set(updateDto.name, savedFieldMetadata);
|
|
1802
|
+
return savedFieldMetadata;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const fieldMetadata = fieldMetadataRepo.create(updateDto as Partial<FieldMetadata>);
|
|
1806
|
+
const savedFieldMetadata = await fieldMetadataRepo.save(fieldMetadata as FieldMetadata) as FieldMetadata;
|
|
1807
|
+
existingFieldsByName.set(updateDto.name, savedFieldMetadata);
|
|
1808
|
+
return savedFieldMetadata;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
910
1811
|
private async handleSeedSecurityRules(rulesDto: CreateSecurityRuleDto[]) {
|
|
911
1812
|
if (!rulesDto || rulesDto.length === 0) {
|
|
912
1813
|
this.logger.debug(`No security rules found to seed`);
|
|
913
1814
|
return;
|
|
914
1815
|
}
|
|
915
1816
|
for (const dto of rulesDto) {
|
|
916
|
-
await this.securityRuleRepo.upsertWithDto({ ...dto, securityRuleConfig: JSON.stringify(dto.securityRuleConfig) })
|
|
1817
|
+
await this.timeOperation('security-rule-upsert', () => this.securityRuleRepo.upsertWithDto({ ...dto, securityRuleConfig: JSON.stringify(dto.securityRuleConfig) }), {
|
|
1818
|
+
component: 'security-rules',
|
|
1819
|
+
serviceCall: 'securityRuleRepo.upsertWithDto',
|
|
1820
|
+
details: `rule=${dto.name}`,
|
|
1821
|
+
});
|
|
917
1822
|
}
|
|
918
1823
|
}
|
|
919
1824
|
|
|
@@ -925,8 +1830,16 @@ export class ModuleMetadataSeederService {
|
|
|
925
1830
|
}
|
|
926
1831
|
for (let j = 0; j < listOfValuesDto.length; j++) {
|
|
927
1832
|
const listOfValueDto = listOfValuesDto[j];
|
|
928
|
-
listOfValueDto['module'] = await this.
|
|
929
|
-
|
|
1833
|
+
listOfValueDto['module'] = await this.getModuleByUserKeyCached(listOfValueDto.moduleUserKey, {
|
|
1834
|
+
moduleName: listOfValueDto.moduleUserKey,
|
|
1835
|
+
component: 'list-of-values',
|
|
1836
|
+
details: `module=${listOfValueDto.moduleUserKey}`,
|
|
1837
|
+
});
|
|
1838
|
+
await this.timeOperation('lov-upsert', () => this.listOfValuesService.upsert(listOfValuesDto[j]), {
|
|
1839
|
+
component: 'list-of-values',
|
|
1840
|
+
serviceCall: 'listOfValuesService.upsert',
|
|
1841
|
+
details: `type=${listOfValueDto.type} value=${listOfValueDto.value}`,
|
|
1842
|
+
});
|
|
930
1843
|
}
|
|
931
1844
|
}
|
|
932
1845
|
|
|
@@ -936,7 +1849,11 @@ export class ModuleMetadataSeederService {
|
|
|
936
1849
|
return;
|
|
937
1850
|
}
|
|
938
1851
|
for (const dto of createScheduledJobDto) {
|
|
939
|
-
await this.scheduledJobRepository.upsertWithDto(dto)
|
|
1852
|
+
await this.timeOperation('scheduled-job-upsert', () => this.scheduledJobRepository.upsertWithDto(dto), {
|
|
1853
|
+
component: 'scheduled-jobs',
|
|
1854
|
+
serviceCall: 'scheduledJobRepository.upsertWithDto',
|
|
1855
|
+
details: `schedule=${dto.scheduleName}`,
|
|
1856
|
+
});
|
|
940
1857
|
}
|
|
941
1858
|
}
|
|
942
1859
|
|
|
@@ -947,7 +1864,11 @@ export class ModuleMetadataSeederService {
|
|
|
947
1864
|
}
|
|
948
1865
|
for (const dto of createSavedFilterDto) {
|
|
949
1866
|
this.validateSavedFilterQueryJsonWrapper(dto);
|
|
950
|
-
await this.savedFiltersRepo.upsertWithDto({ ...dto, filterQueryJson: JSON.stringify(dto.filterQueryJson), isSeeded: true })
|
|
1867
|
+
await this.timeOperation('saved-filter-upsert', () => this.savedFiltersRepo.upsertWithDto({ ...dto, filterQueryJson: JSON.stringify(dto.filterQueryJson), isSeeded: true }), {
|
|
1868
|
+
component: 'saved-filters',
|
|
1869
|
+
serviceCall: 'savedFiltersRepo.upsertWithDto',
|
|
1870
|
+
details: `filter=${dto.name}`,
|
|
1871
|
+
});
|
|
951
1872
|
}
|
|
952
1873
|
}
|
|
953
1874
|
|
|
@@ -976,7 +1897,11 @@ export class ModuleMetadataSeederService {
|
|
|
976
1897
|
return;
|
|
977
1898
|
}
|
|
978
1899
|
for (const dto of modelSequencesDto) {
|
|
979
|
-
await this.modelSequenceRepo.upsertWithDto(dto)
|
|
1900
|
+
await this.timeOperation('model-sequence-upsert', () => this.modelSequenceRepo.upsertWithDto(dto), {
|
|
1901
|
+
component: 'model-sequences',
|
|
1902
|
+
serviceCall: 'modelSequenceRepo.upsertWithDto',
|
|
1903
|
+
details: `sequence=${dto.sequenceName}`,
|
|
1904
|
+
});
|
|
980
1905
|
}
|
|
981
1906
|
}
|
|
982
1907
|
|
|
@@ -987,7 +1912,11 @@ export class ModuleMetadataSeederService {
|
|
|
987
1912
|
}
|
|
988
1913
|
const sequences = modelSequencesDto ?? [];
|
|
989
1914
|
|
|
990
|
-
const module = await this.
|
|
1915
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
1916
|
+
moduleName,
|
|
1917
|
+
component: 'model-sequences',
|
|
1918
|
+
details: `module=${moduleName}`,
|
|
1919
|
+
});
|
|
991
1920
|
if (!module) {
|
|
992
1921
|
this.logger.warn(`Skipping model sequence prune: module not found for ${moduleName}.`);
|
|
993
1922
|
return 0;
|
|
@@ -1026,7 +1955,11 @@ export class ModuleMetadataSeederService {
|
|
|
1026
1955
|
}
|
|
1027
1956
|
const savedFilters = savedFiltersDto ?? [];
|
|
1028
1957
|
|
|
1029
|
-
const module = await this.
|
|
1958
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
1959
|
+
moduleName,
|
|
1960
|
+
component: 'saved-filters',
|
|
1961
|
+
details: `module=${moduleName}`,
|
|
1962
|
+
});
|
|
1030
1963
|
if (!module) {
|
|
1031
1964
|
this.logger.warn(`Skipping saved filters prune: module not found for ${moduleName}.`);
|
|
1032
1965
|
return 0;
|
|
@@ -1066,7 +1999,11 @@ export class ModuleMetadataSeederService {
|
|
|
1066
1999
|
}
|
|
1067
2000
|
const scheduledJobs = scheduledJobsDto ?? [];
|
|
1068
2001
|
|
|
1069
|
-
const module = await this.
|
|
2002
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
2003
|
+
moduleName,
|
|
2004
|
+
component: 'scheduled-jobs',
|
|
2005
|
+
details: `module=${moduleName}`,
|
|
2006
|
+
});
|
|
1070
2007
|
if (!module) {
|
|
1071
2008
|
this.logger.warn(`Skipping scheduled jobs prune: module not found for ${moduleName}.`);
|
|
1072
2009
|
return 0;
|
|
@@ -1105,7 +2042,11 @@ export class ModuleMetadataSeederService {
|
|
|
1105
2042
|
}
|
|
1106
2043
|
const securityRules = securityRulesDto ?? [];
|
|
1107
2044
|
|
|
1108
|
-
const module = await this.
|
|
2045
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
2046
|
+
moduleName,
|
|
2047
|
+
component: 'security-rules',
|
|
2048
|
+
details: `module=${moduleName}`,
|
|
2049
|
+
});
|
|
1109
2050
|
if (!module) {
|
|
1110
2051
|
this.logger.warn(`Skipping security rules prune: module not found for ${moduleName}.`);
|
|
1111
2052
|
return 0;
|
|
@@ -1145,7 +2086,11 @@ export class ModuleMetadataSeederService {
|
|
|
1145
2086
|
}
|
|
1146
2087
|
const listOfValues = listOfValuesDto ?? [];
|
|
1147
2088
|
|
|
1148
|
-
const module = await this.
|
|
2089
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
2090
|
+
moduleName,
|
|
2091
|
+
component: 'list-of-values',
|
|
2092
|
+
details: `module=${moduleName}`,
|
|
2093
|
+
});
|
|
1149
2094
|
if (!module) {
|
|
1150
2095
|
this.logger.warn(`Skipping list of values prune: module not found for ${moduleName}.`);
|
|
1151
2096
|
return 0;
|
|
@@ -1193,7 +2138,11 @@ export class ModuleMetadataSeederService {
|
|
|
1193
2138
|
}
|
|
1194
2139
|
const menus = menusDto ?? [];
|
|
1195
2140
|
|
|
1196
|
-
const module = await this.
|
|
2141
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
2142
|
+
moduleName,
|
|
2143
|
+
component: 'menus',
|
|
2144
|
+
details: `module=${moduleName}`,
|
|
2145
|
+
});
|
|
1197
2146
|
if (!module) {
|
|
1198
2147
|
this.logger.warn(`Skipping menus prune: module not found for ${moduleName}.`);
|
|
1199
2148
|
return 0;
|
|
@@ -1240,7 +2189,11 @@ export class ModuleMetadataSeederService {
|
|
|
1240
2189
|
}
|
|
1241
2190
|
const views = viewsDto ?? [];
|
|
1242
2191
|
|
|
1243
|
-
const module = await this.
|
|
2192
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
2193
|
+
moduleName,
|
|
2194
|
+
component: 'views',
|
|
2195
|
+
details: `module=${moduleName}`,
|
|
2196
|
+
});
|
|
1244
2197
|
if (!module) {
|
|
1245
2198
|
this.logger.warn(`Skipping views prune: module not found for ${moduleName}.`);
|
|
1246
2199
|
return 0;
|
|
@@ -1300,7 +2253,11 @@ export class ModuleMetadataSeederService {
|
|
|
1300
2253
|
}
|
|
1301
2254
|
const actions = actionsDto ?? [];
|
|
1302
2255
|
|
|
1303
|
-
const module = await this.
|
|
2256
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
2257
|
+
moduleName,
|
|
2258
|
+
component: 'actions',
|
|
2259
|
+
details: `module=${moduleName}`,
|
|
2260
|
+
});
|
|
1304
2261
|
if (!module) {
|
|
1305
2262
|
this.logger.warn(`Skipping actions prune: module not found for ${moduleName}.`);
|
|
1306
2263
|
return 0;
|
|
@@ -1398,7 +2355,11 @@ export class ModuleMetadataSeederService {
|
|
|
1398
2355
|
}
|
|
1399
2356
|
const models = modelsMetadata ?? [];
|
|
1400
2357
|
|
|
1401
|
-
const module = await this.
|
|
2358
|
+
const module = await this.getModuleByUserKeyCached(moduleName, {
|
|
2359
|
+
moduleName,
|
|
2360
|
+
component: 'module-model-fields',
|
|
2361
|
+
details: `module=${moduleName}`,
|
|
2362
|
+
});
|
|
1402
2363
|
if (!module) {
|
|
1403
2364
|
this.logger.warn(`Skipping models prune: module not found for ${moduleName}.`);
|
|
1404
2365
|
return 0;
|