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