@solidxai/core 0.1.11-beta.0 → 0.1.11-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/CLAUDE.md +26 -0
- package/dist/commands/migrate-removed-fields.command.d.ts +19 -0
- package/dist/commands/migrate-removed-fields.command.d.ts.map +1 -0
- package/dist/commands/migrate-removed-fields.command.js +77 -0
- package/dist/commands/migrate-removed-fields.command.js.map +1 -0
- package/dist/controllers/datasource-introspection.controller.d.ts +46 -1
- package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -1
- package/dist/controllers/datasource-introspection.controller.js +38 -0
- package/dist/controllers/datasource-introspection.controller.js.map +1 -1
- package/dist/controllers/service.controller.d.ts +1 -0
- package/dist/controllers/service.controller.d.ts.map +1 -1
- package/dist/controllers/service.controller.js +2 -2
- package/dist/controllers/service.controller.js.map +1 -1
- package/dist/dtos/datasource-introspection-mapping.dto.d.ts +5 -0
- package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -1
- package/dist/dtos/datasource-introspection-mapping.dto.js +24 -2
- package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -1
- package/dist/entities/{legacy-common-with-id.entity.d.ts → legacy-common-with-generated-id.entity.d.ts} +2 -2
- package/dist/entities/legacy-common-with-generated-id.entity.d.ts.map +1 -0
- package/dist/entities/{legacy-common-with-id.entity.js → legacy-common-with-generated-id.entity.js} +4 -4
- package/dist/entities/legacy-common-with-generated-id.entity.js.map +1 -0
- package/dist/entities/{legacy-common.entity.d.ts → legacy-common.entity-with-existing-id.entity.d.ts} +1 -1
- package/dist/entities/legacy-common.entity-with-existing-id.entity.d.ts.map +1 -0
- package/dist/entities/{legacy-common.entity.js → legacy-common.entity-with-existing-id.entity.js} +1 -1
- package/dist/entities/legacy-common.entity-with-existing-id.entity.js.map +1 -0
- package/dist/helpers/command.service.d.ts +2 -0
- package/dist/helpers/command.service.d.ts.map +1 -1
- package/dist/helpers/command.service.js +6 -2
- package/dist/helpers/command.service.js.map +1 -1
- package/dist/helpers/environment.helper.d.ts +1 -0
- package/dist/helpers/environment.helper.d.ts.map +1 -1
- package/dist/helpers/environment.helper.js +4 -0
- package/dist/helpers/environment.helper.js.map +1 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.js +4 -1
- package/dist/helpers/field-crud-managers/IntFieldCrudManager.js.map +1 -1
- package/dist/helpers/model-metadata-helper.service.d.ts +2 -1
- package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
- package/dist/helpers/model-metadata-helper.service.js +9 -5
- package/dist/helpers/model-metadata-helper.service.js.map +1 -1
- package/dist/helpers/nodemon-heartbeat.d.ts +5 -0
- package/dist/helpers/nodemon-heartbeat.d.ts.map +1 -0
- package/dist/helpers/nodemon-heartbeat.js +67 -0
- package/dist/helpers/nodemon-heartbeat.js.map +1 -0
- package/dist/helpers/solid-registry.d.ts +2 -1
- package/dist/helpers/solid-registry.d.ts.map +1 -1
- package/dist/helpers/solid-registry.js.map +1 -1
- package/dist/helpers/typeorm-db-helper.d.ts +1 -0
- package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
- package/dist/helpers/typeorm-db-helper.js +12 -0
- package/dist/helpers/typeorm-db-helper.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/interfaces/datasource-introspection-provider.interface.d.ts +1 -0
- package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -1
- package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -1
- package/dist/interfaces.d.ts +1 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/repository/scheduled-job.repository.d.ts +5 -2
- package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
- package/dist/repository/scheduled-job.repository.js +66 -3
- package/dist/repository/scheduled-job.repository.js.map +1 -1
- package/dist/repository/security-rule.repository.d.ts +2 -2
- package/dist/repository/security-rule.repository.d.ts.map +1 -1
- package/dist/repository/security-rule.repository.js +6 -3
- package/dist/repository/security-rule.repository.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts +43 -2
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +1173 -232
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +128 -34
- package/dist/services/action-metadata.service.d.ts +7 -0
- package/dist/services/action-metadata.service.d.ts.map +1 -1
- package/dist/services/action-metadata.service.js +76 -3
- package/dist/services/action-metadata.service.js.map +1 -1
- package/dist/services/api-key.service.d.ts.map +1 -1
- package/dist/services/api-key.service.js.map +1 -1
- package/dist/services/authentication.service.d.ts +2 -0
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +32 -11
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +7 -5
- package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +4 -0
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -1
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +18 -1
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -1
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +25 -12
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/crud.service.d.ts +1 -0
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +27 -0
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +12 -0
- package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -1
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +2 -0
- package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -1
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +28 -15
- package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -1
- package/dist/services/datasource-introspection.service.d.ts +62 -2
- package/dist/services/datasource-introspection.service.d.ts.map +1 -1
- package/dist/services/datasource-introspection.service.js +654 -186
- package/dist/services/datasource-introspection.service.js.map +1 -1
- package/dist/services/import-transaction.service.d.ts.map +1 -1
- package/dist/services/import-transaction.service.js +3 -0
- package/dist/services/import-transaction.service.js.map +1 -1
- package/dist/services/locale.service.d.ts +14 -0
- package/dist/services/locale.service.d.ts.map +1 -1
- package/dist/services/locale.service.js +83 -0
- package/dist/services/locale.service.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +10 -10
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
- package/dist/services/menu-item-metadata.service.d.ts.map +1 -1
- package/dist/services/menu-item-metadata.service.js +1 -0
- package/dist/services/menu-item-metadata.service.js.map +1 -1
- package/dist/services/model-metadata.service.d.ts +3 -0
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +78 -0
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/module-metadata.service.d.ts.map +1 -1
- package/dist/services/module-metadata.service.js +28 -0
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/removed-field-migration.service.d.ts +30 -0
- package/dist/services/removed-field-migration.service.d.ts.map +1 -0
- package/dist/services/removed-field-migration.service.js +319 -0
- package/dist/services/removed-field-migration.service.js.map +1 -0
- package/dist/services/role-metadata.service.d.ts +5 -0
- package/dist/services/role-metadata.service.d.ts.map +1 -1
- package/dist/services/role-metadata.service.js +95 -33
- package/dist/services/role-metadata.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +244 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +120 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/solid-ts-morph.service.d.ts +6 -0
- package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
- package/dist/services/solid-ts-morph.service.js +43 -0
- package/dist/services/solid-ts-morph.service.js.map +1 -1
- package/dist/services/view-metadata.service.d.ts +6 -0
- package/dist/services/view-metadata.service.d.ts.map +1 -1
- package/dist/services/view-metadata.service.js +70 -8
- package/dist/services/view-metadata.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +4 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/audit.subscriber.d.ts +5 -1
- package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
- package/dist/subscribers/audit.subscriber.js +76 -12
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts +9 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.js +45 -11
- package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
- package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
- package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
- package/dist/subscribers/scheduled-job.subscriber.d.ts.map +1 -1
- package/dist/subscribers/scheduled-job.subscriber.js +3 -2
- package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
- package/dist/subscribers/security-rule.subscriber.d.ts.map +1 -1
- package/dist/subscribers/security-rule.subscriber.js +5 -3
- package/dist/subscribers/security-rule.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/migrate-removed-fields.command.ts +81 -0
- package/src/controllers/datasource-introspection.controller.ts +27 -1
- package/src/controllers/service.controller.ts +3 -2
- package/src/dtos/datasource-introspection-mapping.dto.ts +15 -0
- package/src/entities/{legacy-common-with-id.entity.ts → legacy-common-with-generated-id.entity.ts} +1 -1
- package/src/helpers/command.service.ts +10 -2
- package/src/helpers/environment.helper.ts +11 -0
- package/src/helpers/field-crud-managers/IntFieldCrudManager.ts +4 -1
- package/src/helpers/model-metadata-helper.service.ts +8 -3
- package/src/helpers/nodemon-heartbeat.ts +59 -0
- package/src/helpers/solid-registry.ts +7 -1
- package/src/helpers/typeorm-db-helper.ts +15 -0
- package/src/index.ts +3 -2
- package/src/interfaces/datasource-introspection-provider.interface.ts +1 -0
- package/src/interfaces.ts +1 -0
- package/src/repository/scheduled-job.repository.ts +83 -4
- package/src/repository/security-rule.repository.ts +10 -4
- package/src/seeders/module-metadata-seeder.service.ts +1415 -264
- package/src/seeders/seed-data/solid-core-metadata.json +128 -34
- package/src/services/action-metadata.service.ts +87 -2
- package/src/services/api-key.service.ts +36 -36
- package/src/services/authentication.service.ts +46 -12
- package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +11 -5
- package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +15 -1
- package/src/services/crud-helper.service.ts +25 -12
- package/src/services/crud.service.ts +36 -11
- package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +12 -0
- package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +2 -0
- package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +28 -15
- package/src/services/datasource-introspection.service.ts +811 -161
- package/src/services/import-transaction.service.ts +5 -0
- package/src/services/locale.service.ts +111 -3
- package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +2 -2
- package/src/services/menu-item-metadata.service.ts +2 -1
- package/src/services/model-metadata.service.ts +109 -1
- package/src/services/module-metadata.service.ts +36 -0
- package/src/services/removed-field-migration.service.ts +334 -0
- package/src/services/role-metadata.service.ts +112 -49
- package/src/services/settings/default-settings-provider.service.ts +120 -0
- package/src/services/solid-ts-morph.service.ts +61 -1
- package/src/services/view-metadata.service.ts +83 -9
- package/src/solid-core.module.ts +4 -0
- package/src/subscribers/audit.subscriber.ts +101 -18
- package/src/subscribers/computed-entity-field.subscriber.ts +81 -28
- package/src/subscribers/created-by-updated-by.subscriber.ts +3 -0
- package/src/subscribers/field-metadata.subscriber.ts +3 -0
- package/src/subscribers/scheduled-job.subscriber.ts +3 -2
- package/src/subscribers/security-rule.subscriber.ts +8 -3
- package/dist/entities/legacy-common-with-id.entity.d.ts.map +0 -1
- package/dist/entities/legacy-common-with-id.entity.js.map +0 -1
- package/dist/entities/legacy-common.entity.d.ts.map +0 -1
- package/dist/entities/legacy-common.entity.js.map +0 -1
- /package/src/entities/{legacy-common.entity.ts → legacy-common.entity-with-existing-id.entity.ts} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Injectable, Logger } from "@nestjs/common";
|
|
3
3
|
import { join, dirname, normalize, isAbsolute, basename } from "node:path";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { Project, Node, ObjectLiteralExpression, ArrayLiteralExpression, QuoteKind, IndentationText } from "ts-morph";
|
|
5
|
+
import { Project, Node, ObjectLiteralExpression, ArrayLiteralExpression, QuoteKind, IndentationText, SyntaxKind } from "ts-morph";
|
|
6
6
|
import { MethodDeclarationStructure, StructureKind } from "ts-morph";
|
|
7
7
|
|
|
8
8
|
type Bucket = "imports" | "providers" | "exports";
|
|
@@ -468,6 +468,66 @@ export class SolidTsMorphService {
|
|
|
468
468
|
return { staged: true, overwritten: false, skipped: false };
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
+
cleanupTypeormDatasourceEntity(
|
|
472
|
+
filePath: string,
|
|
473
|
+
entityImportPath: string,
|
|
474
|
+
entityClassName: string,
|
|
475
|
+
): { staged: boolean; skipped: boolean; removedImport: boolean; removedEntity: boolean } {
|
|
476
|
+
const abs = this.resolveRepoPath(filePath);
|
|
477
|
+
if (!existsSync(abs)) {
|
|
478
|
+
this.logger.warn(`cleanupTypeormDatasourceEntity: file not found at ${filePath}, skipping.`);
|
|
479
|
+
return { staged: false, skipped: true, removedImport: false, removedEntity: false };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const existing = this.project.getSourceFile(abs);
|
|
483
|
+
const sourceFile = existing
|
|
484
|
+
? existing
|
|
485
|
+
: this.project.createSourceFile(abs, readFileSync(abs, "utf8"), { overwrite: true });
|
|
486
|
+
|
|
487
|
+
let removedImport = false;
|
|
488
|
+
let removedEntity = false;
|
|
489
|
+
|
|
490
|
+
const importDeclaration = sourceFile.getImportDeclarations().find((declaration) =>
|
|
491
|
+
declaration.getModuleSpecifierValue().replace(/\\/g, "/") === entityImportPath.replace(/\\/g, "/"),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
if (importDeclaration) {
|
|
495
|
+
const namedImport = importDeclaration.getNamedImports().find((named) => named.getName() === entityClassName);
|
|
496
|
+
if (namedImport) {
|
|
497
|
+
namedImport.remove();
|
|
498
|
+
removedImport = true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (
|
|
502
|
+
importDeclaration.getNamedImports().length === 0 &&
|
|
503
|
+
!importDeclaration.getDefaultImport() &&
|
|
504
|
+
!importDeclaration.getNamespaceImport()
|
|
505
|
+
) {
|
|
506
|
+
importDeclaration.remove();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const entitiesDeclaration = sourceFile.getVariableDeclaration("entities");
|
|
511
|
+
const entitiesArray = entitiesDeclaration?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
|
512
|
+
if (entitiesArray) {
|
|
513
|
+
const elements = entitiesArray.getElements();
|
|
514
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
515
|
+
if (elements[i].getText().replace(/\s+/g, "") === entityClassName) {
|
|
516
|
+
entitiesArray.removeElement(i);
|
|
517
|
+
removedEntity = true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!removedImport && !removedEntity) {
|
|
523
|
+
return { staged: false, skipped: true, removedImport, removedEntity };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
this.dirtySourceFiles.add(abs);
|
|
527
|
+
this.logger.log(`Staged datasource cleanup in ${this.rel(abs)} for entity ${entityClassName}`);
|
|
528
|
+
return { staged: true, skipped: false, removedImport, removedEntity };
|
|
529
|
+
}
|
|
530
|
+
|
|
471
531
|
|
|
472
532
|
|
|
473
533
|
}
|
|
@@ -21,6 +21,74 @@ import { MenuItemMetadata } from 'src/entities/menu-item-metadata.entity';
|
|
|
21
21
|
|
|
22
22
|
@Injectable()
|
|
23
23
|
export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
24
|
+
private readonly comparableFields = [
|
|
25
|
+
'name',
|
|
26
|
+
'displayName',
|
|
27
|
+
'type',
|
|
28
|
+
'context',
|
|
29
|
+
'layout',
|
|
30
|
+
'module',
|
|
31
|
+
'model',
|
|
32
|
+
] as const;
|
|
33
|
+
private readonly relationCompareFields = new Set(['module', 'model']);
|
|
34
|
+
private readonly jsonCompareFields = new Set(['layout', 'context']);
|
|
35
|
+
|
|
36
|
+
private normalizeJsonFieldValue(value: any): any {
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
const trimmedValue = value.trim();
|
|
39
|
+
if (
|
|
40
|
+
(trimmedValue.startsWith('{') && trimmedValue.endsWith('}')) ||
|
|
41
|
+
(trimmedValue.startsWith('[') && trimmedValue.endsWith(']'))
|
|
42
|
+
) {
|
|
43
|
+
try {
|
|
44
|
+
return this.normalizeJsonFieldValue(JSON.parse(trimmedValue));
|
|
45
|
+
} catch {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.map((item) => this.normalizeJsonFieldValue(item));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (value && typeof value === 'object') {
|
|
57
|
+
return Object.keys(value)
|
|
58
|
+
.sort()
|
|
59
|
+
.reduce((result, key) => {
|
|
60
|
+
result[key] = this.normalizeJsonFieldValue(value[key]);
|
|
61
|
+
return result;
|
|
62
|
+
}, {} as Record<string, any>);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private getCanonicalJsonFieldString(value: any): string {
|
|
69
|
+
return JSON.stringify(this.normalizeJsonFieldValue(value) ?? null);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private hasChanges(existingSolidView: ViewMetadata, updateSolidViewDto: UpdateViewMetadataDto): boolean {
|
|
73
|
+
return this.comparableFields.some((key) => {
|
|
74
|
+
const value = (updateSolidViewDto as any)[key];
|
|
75
|
+
if (typeof value === 'undefined') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.relationCompareFields.has(key)) {
|
|
80
|
+
const relationValue = value as any;
|
|
81
|
+
return (existingSolidView as any)[key]?.id !== relationValue?.id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this.jsonCompareFields.has(key)) {
|
|
85
|
+
return this.getCanonicalJsonFieldString((existingSolidView as any)[key]) !== this.getCanonicalJsonFieldString(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (existingSolidView as any)[key] !== value;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
24
92
|
constructor(
|
|
25
93
|
readonly actionMetadataService: ActionMetadataService,
|
|
26
94
|
readonly menuItemMetadataService: MenuItemMetadataService,
|
|
@@ -83,15 +151,11 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
83
151
|
return { records: [], defaultEntityLocaleId: null };
|
|
84
152
|
}
|
|
85
153
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (entityRecord.localeName === defaultLocale?.locale) {
|
|
90
|
-
defaultEntityLocaleId = entityRecord.id;
|
|
91
|
-
this.logger.debug(`Editing default locale record with id ${defaultEntityLocaleId}`);
|
|
154
|
+
const defaultEntityLocaleId = entityRecord.defaultEntityLocaleId || entityRecord.id;
|
|
155
|
+
if (entityRecord.defaultEntityLocaleId) {
|
|
156
|
+
this.logger.debug(`Editing translated locale record. Translation root id: ${defaultEntityLocaleId}`);
|
|
92
157
|
} else {
|
|
93
|
-
|
|
94
|
-
this.logger.debug(`Editing non-default locale record. DefaultEntityLocaleId: ${defaultEntityLocaleId}`);
|
|
158
|
+
this.logger.debug(`Editing translation root record with id ${defaultEntityLocaleId}`);
|
|
95
159
|
}
|
|
96
160
|
|
|
97
161
|
const records = await currentEntityRepository.find({
|
|
@@ -438,10 +502,20 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
438
502
|
|
|
439
503
|
async upsert(updateSolidViewDto: UpdateViewMetadataDto) {
|
|
440
504
|
// First check if module already exists using name
|
|
441
|
-
const existingSolidView = await this.findOneByUserKey(updateSolidViewDto.name
|
|
505
|
+
const existingSolidView = await this.findOneByUserKey(updateSolidViewDto.name, {
|
|
506
|
+
module: true,
|
|
507
|
+
model: true,
|
|
508
|
+
});
|
|
442
509
|
|
|
443
510
|
// if found
|
|
444
511
|
if (existingSolidView) {
|
|
512
|
+
const hasChanges = this.hasChanges(existingSolidView, updateSolidViewDto);
|
|
513
|
+
|
|
514
|
+
if (!hasChanges) {
|
|
515
|
+
this.logger.debug(`Skipping view upsert for ${updateSolidViewDto.name}; no changes detected.`);
|
|
516
|
+
return existingSolidView;
|
|
517
|
+
}
|
|
518
|
+
|
|
445
519
|
const updatedSolidViewDto = { ...existingSolidView, ...updateSolidViewDto };
|
|
446
520
|
return this.repo.save(updatedSolidViewDto);
|
|
447
521
|
}
|
package/src/solid-core.module.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { MulterModule } from "@nestjs/platform-express";
|
|
14
14
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
15
15
|
import { RemoveFieldsCommand } from "./commands/remove-fields.command";
|
|
16
|
+
import { MigrateRemovedFieldsCommand } from "./commands/migrate-removed-fields.command";
|
|
16
17
|
import { FieldMetadataController } from "./controllers/field-metadata.controller";
|
|
17
18
|
import { DashboardController } from "./controllers/dashboard.controller";
|
|
18
19
|
import { MediaStorageProviderMetadataController } from "./controllers/media-storage-provider-metadata.controller";
|
|
@@ -47,6 +48,7 @@ import { ListOfValuesService } from "./services/list-of-values.service";
|
|
|
47
48
|
import { MediaStorageProviderMetadataService } from "./services/media-storage-provider-metadata.service";
|
|
48
49
|
import { MediaService } from "./services/media.service";
|
|
49
50
|
import { ModelMetadataService } from "./services/model-metadata.service";
|
|
51
|
+
import { RemovedFieldMigrationService } from "./services/removed-field-migration.service";
|
|
50
52
|
import { ModuleMetadataExplorerService } from "./services/module-metadata-explorer.service";
|
|
51
53
|
import { ModuleMetadataService } from "./services/module-metadata.service";
|
|
52
54
|
import { ModulePackageService } from "./services/module-package.service";
|
|
@@ -545,6 +547,7 @@ import { PostgresDatasourceIntrospectionProviderService } from "./services/datas
|
|
|
545
547
|
FieldMetadataService,
|
|
546
548
|
DashboardRuntimeService,
|
|
547
549
|
RemoveFieldsCommand,
|
|
550
|
+
MigrateRemovedFieldsCommand,
|
|
548
551
|
RefreshModelCommand,
|
|
549
552
|
RefreshModuleCommand,
|
|
550
553
|
InfoCommand,
|
|
@@ -791,6 +794,7 @@ import { PostgresDatasourceIntrospectionProviderService } from "./services/datas
|
|
|
791
794
|
UserViewMetadataRepository,
|
|
792
795
|
ModelMetadataRepository,
|
|
793
796
|
ModuleMetadataRepository,
|
|
797
|
+
RemovedFieldMigrationService,
|
|
794
798
|
ActionMetadataRepository,
|
|
795
799
|
MediaStorageProviderMetadataRepository,
|
|
796
800
|
FixturesService,
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Injectable, Logger, Scope } from '@nestjs/common';
|
|
2
|
+
import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
|
|
2
3
|
import { lowerFirst } from 'src/helpers/string.helper';
|
|
3
4
|
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
4
5
|
import { DataSource, EntityMetadata, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
|
|
5
6
|
import { AuditQueuePayload } from 'src/interfaces';
|
|
7
|
+
import { isEmbeddedDb } from 'src/helpers/environment.helper';
|
|
6
8
|
import { RequestContextService } from 'src/services/request-context.service';
|
|
7
9
|
import { PublisherFactory } from 'src/services/queues/publisher-factory.service';
|
|
10
|
+
const AUDIT_BEFORE_SNAPSHOT = '__auditBeforeSnapshot';
|
|
8
11
|
|
|
9
|
-
@Injectable({scope: Scope.TRANSIENT})
|
|
12
|
+
@Injectable({ scope: Scope.TRANSIENT })
|
|
10
13
|
export class AuditSubscriber implements EntitySubscriberInterface {
|
|
11
14
|
private readonly logger = new Logger(AuditSubscriber.name);
|
|
12
15
|
private dataSource: DataSource;
|
|
@@ -14,6 +17,7 @@ export class AuditSubscriber implements EntitySubscriberInterface {
|
|
|
14
17
|
private readonly publisherFactory: PublisherFactory<AuditQueuePayload>,
|
|
15
18
|
private readonly solidRegistry: SolidRegistry,
|
|
16
19
|
private readonly requestContextService: RequestContextService,
|
|
20
|
+
private readonly modelMetadataHelperService: ModelMetadataHelperService,
|
|
17
21
|
) { }
|
|
18
22
|
|
|
19
23
|
bindToDataSource(dataSource: DataSource) {
|
|
@@ -53,20 +57,85 @@ export class AuditSubscriber implements EntitySubscriberInterface {
|
|
|
53
57
|
|
|
54
58
|
async afterUpdate(event: UpdateEvent<any>) {
|
|
55
59
|
if (!this.shouldTrackAudit(event.metadata)) return;
|
|
60
|
+
const entityId = event.entity?.id ?? event.databaseEntity?.id ?? (event as any).entityId ?? null;
|
|
61
|
+
let before = event.databaseEntity ?? null;
|
|
62
|
+
let after = event.entity ?? null;
|
|
63
|
+
const updatedColumnNames = (event.updatedColumns ?? []).map(c => c.propertyName);
|
|
64
|
+
|
|
65
|
+
const auditResult = await this.prepareManyToManyAuditUpdateSnapshot(event, entityId, updatedColumnNames);
|
|
66
|
+
|
|
67
|
+
if (auditResult) {
|
|
68
|
+
before = auditResult.before;
|
|
69
|
+
after = auditResult.after;
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
this.enqueue(event, {
|
|
57
73
|
eventType: 'update',
|
|
58
74
|
modelName: event.metadata.name,
|
|
59
|
-
entityId:
|
|
75
|
+
entityId: entityId,
|
|
60
76
|
occurredAt: new Date().toISOString(),
|
|
61
|
-
after:
|
|
77
|
+
after: after,
|
|
62
78
|
// databaseEntity is only populated when the entity was fetched first (save() path).
|
|
63
79
|
// QueryBuilder update() leaves this undefined; postAuditMessageOnUpdate guards for it.
|
|
64
|
-
before:
|
|
65
|
-
updatedColumnNames:
|
|
80
|
+
before: before,
|
|
81
|
+
updatedColumnNames: updatedColumnNames,
|
|
66
82
|
userId: this.activeUserId(),
|
|
67
83
|
});
|
|
68
84
|
}
|
|
69
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Resolves before/after snapshots for audit-enabled many-to-many relations
|
|
88
|
+
* and updates changed relation field names for audit tracking.
|
|
89
|
+
*/
|
|
90
|
+
private async prepareManyToManyAuditUpdateSnapshot(event: UpdateEvent<any>, entityId: number | null, updatedColumnNames: string[]): Promise<{ before: any; after: any } | null> {
|
|
91
|
+
|
|
92
|
+
// On embedded PGlite, pass the transaction's manager so the query runs
|
|
93
|
+
// on the active connection. On regular Postgres, use the default DataSource.
|
|
94
|
+
const mgr = isEmbeddedDb() ? event.manager : this.dataSource;
|
|
95
|
+
const auditRelationFields = (await this.modelMetadataHelperService.loadFieldHierarchy(lowerFirst(event.metadata.name), isEmbeddedDb() ? event.manager : undefined)).filter(field =>
|
|
96
|
+
field.enableAuditTracking &&
|
|
97
|
+
field.type === 'relation' &&
|
|
98
|
+
field.relationType !== 'one-to-many'
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!entityId || auditRelationFields.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const relations: Record<string, boolean> = {};
|
|
106
|
+
|
|
107
|
+
auditRelationFields.forEach(field => {
|
|
108
|
+
relations[field.name] = true;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const relationBefore = event.entity?.[AUDIT_BEFORE_SNAPSHOT] ?? null;
|
|
112
|
+
|
|
113
|
+
const relationAfter = await mgr.getRepository(event.metadata.target as any).findOne({
|
|
114
|
+
where: { id: entityId } as any,
|
|
115
|
+
relations: relations as any,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (relationBefore && relationAfter) {
|
|
119
|
+
auditRelationFields.forEach(field => {
|
|
120
|
+
const oldIds = Array.isArray(relationBefore[field.name])
|
|
121
|
+
? relationBefore[field.name].map(item => item.id).sort()
|
|
122
|
+
: [];
|
|
123
|
+
|
|
124
|
+
const newIds = Array.isArray(relationAfter[field.name])
|
|
125
|
+
? relationAfter[field.name].map(item => item.id).sort()
|
|
126
|
+
: [];
|
|
127
|
+
|
|
128
|
+
if ((oldIds.length !== newIds.length || JSON.stringify(oldIds) !== JSON.stringify(newIds))
|
|
129
|
+
&& !updatedColumnNames.includes(field.name)) {
|
|
130
|
+
updatedColumnNames.push(field.name);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
before: relationBefore ?? event.databaseEntity ?? null,
|
|
136
|
+
after: relationAfter ?? event.entity ?? null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
70
139
|
async afterRemove(event: RemoveEvent<any>) {
|
|
71
140
|
if (!this.shouldTrackAudit(event.metadata)) return;
|
|
72
141
|
this.enqueue(event, {
|
|
@@ -84,24 +153,38 @@ export class AuditSubscriber implements EntitySubscriberInterface {
|
|
|
84
153
|
const batch = this.perTxn.get(event.queryRunner) ?? [];
|
|
85
154
|
this.perTxn.delete(event.queryRunner);
|
|
86
155
|
|
|
87
|
-
|
|
88
|
-
// allSettled: publish in parallel; a single failure does not block the rest.
|
|
89
|
-
const results = await Promise.allSettled(
|
|
90
|
-
batch.map(payload => this.publisherFactory.publish({ payload }, 'ChatterQueuePublisher'))
|
|
91
|
-
);
|
|
156
|
+
if (batch.length === 0) return;
|
|
92
157
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
});
|
|
158
|
+
if (isEmbeddedDb()) {
|
|
159
|
+
setImmediate(() => void this.publishAuditBatch(batch));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await this.publishAuditBatch(batch);
|
|
101
164
|
}
|
|
102
165
|
|
|
103
166
|
afterTransactionRollback(event: { queryRunner: any }) {
|
|
104
167
|
// Drop buffered payloads; the write never happened.
|
|
105
168
|
this.perTxn.delete(event.queryRunner);
|
|
106
169
|
}
|
|
170
|
+
|
|
171
|
+
private async publishAuditBatch(batch: AuditQueuePayload[]) {
|
|
172
|
+
try {
|
|
173
|
+
// allSettled: publish in parallel; a single failure does not block the rest.
|
|
174
|
+
const results = await Promise.allSettled(
|
|
175
|
+
batch.map(payload => this.publisherFactory.publish({ payload }, 'ChatterQueuePublisher'))
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
results.forEach((result, i) => {
|
|
179
|
+
if (result.status === 'rejected') {
|
|
180
|
+
this.logger.error(
|
|
181
|
+
`Failed to publish audit event for ${batch[i].modelName}#${batch[i].entityId}`,
|
|
182
|
+
result.reason,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.logger.error('Failed to publish audit batch', error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
107
190
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { camelCase } from 'lodash';
|
|
2
|
-
import { Delete } from "@aws-sdk/client-s3";
|
|
3
2
|
import { forwardRef, Inject, Injectable, InternalServerErrorException, Logger, Scope } from "@nestjs/common";
|
|
4
|
-
import { model } from "mongoose";
|
|
5
3
|
import { ComputedFieldTriggerOperation } from "src/dtos/create-field-metadata.dto";
|
|
4
|
+
import { isEmbeddedDb } from "src/helpers/environment.helper";
|
|
6
5
|
import { ComputedFieldMetadata, SolidRegistry, TypeOrmEventContext } from "src/helpers/solid-registry";
|
|
7
6
|
import { IEntityPreComputeFieldProvider } from "src/interfaces";
|
|
8
7
|
import { PublisherFactory } from "src/services/queues/publisher-factory.service";
|
|
@@ -14,19 +13,22 @@ export interface ComputedFieldEvaluationPayload extends ComputedFieldMetadata {
|
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
@Injectable({ scope: Scope.TRANSIENT })
|
|
17
|
-
// @EventSubscriber()
|
|
18
16
|
export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface {
|
|
19
17
|
private readonly logger = new Logger(this.constructor.name);
|
|
20
18
|
private dataSource: DataSource;
|
|
19
|
+
|
|
20
|
+
// Per-transaction buffer for post-event evaluation jobs. Entries are
|
|
21
|
+
// flushed in afterTransactionCommit so the publish (which uses the
|
|
22
|
+
// default DataSource) never races the in-flight transaction's connection.
|
|
23
|
+
// On PGlite (pool size 1), the flush is deferred via setImmediate to
|
|
24
|
+
// ensure the transaction's connection has been released first.
|
|
25
|
+
private perTxn = new WeakMap<any, { computedField: ComputedFieldMetadata<any>; databaseEntity: any }[]>();
|
|
26
|
+
|
|
21
27
|
constructor(
|
|
22
|
-
// @InjectDataSource()
|
|
23
|
-
// private readonly dataSource: DataSource,
|
|
24
28
|
private readonly solidRegistry: SolidRegistry,
|
|
25
29
|
@Inject(forwardRef(() => PublisherFactory))
|
|
26
30
|
private readonly publisherFactory: PublisherFactory<ComputedFieldEvaluationPayload>
|
|
27
|
-
// private readonly computedFieldPublisher: ComputedFieldEvaluationPublisherDatabase,
|
|
28
31
|
) {
|
|
29
|
-
// this.dataSource.subscribers.push(this);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
bindToDataSource(dataSource: DataSource) {
|
|
@@ -43,47 +45,97 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface
|
|
|
43
45
|
async beforeUpdate(event: UpdateEvent<any>): Promise<any> {
|
|
44
46
|
const modelName = camelCase(event.metadata?.name ?? event.entity?.constructor?.name ?? '');
|
|
45
47
|
const eventContext = this.sanitizeEventContext(event, 'beforeUpdate');
|
|
46
|
-
// await this.handleComputedFieldEvaluation(event.databaseEntity, ComputedFieldTriggerOperation.beforeUpdate, modelName, eventContext);
|
|
47
48
|
await this.handleComputedFieldEvaluation(event.entity, ComputedFieldTriggerOperation.beforeUpdate, modelName, eventContext);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
afterInsert(event: InsertEvent<any>) {
|
|
51
52
|
const modelName = camelCase(event.metadata?.name ?? event.entity?.constructor?.name ?? '');
|
|
52
53
|
const eventContext = this.sanitizeEventContext(event, 'afterInsert');
|
|
53
|
-
this.
|
|
54
|
+
this.handlePostEventJobs(event, event.entity, ComputedFieldTriggerOperation.afterInsert, modelName, eventContext);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
afterUpdate(event: UpdateEvent<any>) {
|
|
57
58
|
const modelName = camelCase(event.metadata?.name ?? event.entity?.constructor?.name ?? event.databaseEntity?.constructor?.name ?? '');
|
|
58
59
|
const eventContext = this.sanitizeEventContext(event, 'afterUpdate');
|
|
59
|
-
|
|
60
|
-
this.handleComputedFieldEvaluationJob(event.entity, ComputedFieldTriggerOperation.afterUpdate, modelName, eventContext);
|
|
60
|
+
this.handlePostEventJobs(event, event.entity, ComputedFieldTriggerOperation.afterUpdate, modelName, eventContext);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
afterRemove(event: RemoveEvent<any>) {
|
|
64
64
|
const modelName = camelCase(event.metadata?.name ?? event.entity?.constructor?.name ?? event.databaseEntity?.constructor?.name ?? '');
|
|
65
65
|
const eventContext = this.sanitizeEventContext(event, 'afterRemove');
|
|
66
|
-
this.
|
|
66
|
+
this.handlePostEventJobs(event, event.databaseEntity, ComputedFieldTriggerOperation.afterRemove, modelName, eventContext);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
//FIXME: Need to add support for beforeRemove, beforeSoftRemove, afterSoftRemove, beforeRecover, afterRecover
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
// --------- post-event dispatch (pglite vs non-pglite) ----------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* On embedded PGlite (pool size 1): buffer payloads on the queryRunner and
|
|
75
|
+
* flush in afterTransactionCommit (deferred via setImmediate) so the publish
|
|
76
|
+
* never tries to get a second connection while the transaction is active.
|
|
77
|
+
*
|
|
78
|
+
* On regular Postgres: fire-and-forget publish immediately (original behaviour),
|
|
79
|
+
* since the pool has spare connections.
|
|
80
|
+
*/
|
|
81
|
+
private handlePostEventJobs(
|
|
82
|
+
event: InsertEvent<any> | UpdateEvent<any> | RemoveEvent<any>,
|
|
83
|
+
entity: any,
|
|
84
|
+
currentOperation: ComputedFieldTriggerOperation,
|
|
85
|
+
modelName: string,
|
|
86
|
+
eventContext?: TypeOrmEventContext,
|
|
87
|
+
) {
|
|
88
|
+
if (!entity) return;
|
|
89
|
+
|
|
75
90
|
const computedFieldsTobeEvaluated = this.getComputedFieldsForEvaluation(
|
|
76
91
|
this.solidRegistry.getComputedFieldMetadata(),
|
|
77
92
|
currentOperation,
|
|
78
93
|
modelName
|
|
79
94
|
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
if (computedFieldsTobeEvaluated.length === 0) return;
|
|
96
|
+
|
|
97
|
+
if (isEmbeddedDb()) {
|
|
98
|
+
const qr = event.queryRunner;
|
|
99
|
+
const arr = this.perTxn.get(qr) ?? [];
|
|
100
|
+
for (const computedField of computedFieldsTobeEvaluated) {
|
|
101
|
+
arr.push({
|
|
102
|
+
computedField: this.attachContext(computedField, eventContext),
|
|
103
|
+
databaseEntity: entity,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
this.perTxn.set(qr, arr);
|
|
107
|
+
} else {
|
|
108
|
+
for (const computedField of computedFieldsTobeEvaluated) {
|
|
109
|
+
this.enqueueComputedFieldEvaluationJob(
|
|
110
|
+
this.attachContext(computedField, eventContext),
|
|
111
|
+
entity,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
83
114
|
}
|
|
84
115
|
}
|
|
85
116
|
|
|
86
|
-
|
|
117
|
+
// --------- transaction lifecycle (pglite only) ----------
|
|
118
|
+
async afterTransactionCommit(event: { queryRunner: any }) {
|
|
119
|
+
const batch = this.perTxn.get(event.queryRunner) ?? [];
|
|
120
|
+
this.perTxn.delete(event.queryRunner);
|
|
121
|
+
if (batch.length === 0) return;
|
|
122
|
+
|
|
123
|
+
// Deferred via setImmediate so the transaction's connection is released
|
|
124
|
+
// back to the (size-1) pool before the publish tries to acquire it.
|
|
125
|
+
setImmediate(() => void this.flushBatch(batch));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
afterTransactionRollback(event: { queryRunner: any }) {
|
|
129
|
+
this.perTxn.delete(event.queryRunner);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async flushBatch(batch: { computedField: ComputedFieldMetadata<any>; databaseEntity: any }[]) {
|
|
133
|
+
for (const { computedField, databaseEntity } of batch) {
|
|
134
|
+
this.enqueueComputedFieldEvaluationJob(computedField, databaseEntity);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async handleComputedFieldEvaluation(entity: any, currentOperation: ComputedFieldTriggerOperation, modelName: string, eventContext?: TypeOrmEventContext): Promise<void> {
|
|
87
139
|
if (!entity) {
|
|
88
140
|
return;
|
|
89
141
|
}
|
|
@@ -92,9 +144,8 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface
|
|
|
92
144
|
currentOperation,
|
|
93
145
|
modelName
|
|
94
146
|
);
|
|
95
|
-
//TODO: We can add a feature i.e dependsOn, where we can check if the computed field depends on other computed fields and evaluate them first
|
|
96
147
|
for (const computedField of computedFieldsTobeEvaluated) {
|
|
97
|
-
this.
|
|
148
|
+
await this.evaluateComputedField(this.attachContext(computedField, eventContext), entity, currentOperation);
|
|
98
149
|
}
|
|
99
150
|
}
|
|
100
151
|
|
|
@@ -147,16 +198,14 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface
|
|
|
147
198
|
}
|
|
148
199
|
}
|
|
149
200
|
|
|
150
|
-
private enqueueComputedFieldEvaluationJob(computedField: ComputedFieldMetadata<any>, databaseEntity: any
|
|
201
|
+
private enqueueComputedFieldEvaluationJob(computedField: ComputedFieldMetadata<any>, databaseEntity: any) {
|
|
202
|
+
const { manager: _manager, ...serializableEventContext } = computedField.eventContext ?? {};
|
|
151
203
|
const payload = {
|
|
152
204
|
...computedField,
|
|
205
|
+
eventContext: serializableEventContext,
|
|
153
206
|
databaseEntity,
|
|
154
|
-
// eventContext,
|
|
155
207
|
};
|
|
156
|
-
this.publisherFactory.publish({ payload }, 'ComputedFieldEvaluationPublisher')
|
|
157
|
-
// this.computedFieldPublisher.publish({
|
|
158
|
-
// payload
|
|
159
|
-
// });
|
|
208
|
+
this.publisherFactory.publish({ payload }, 'ComputedFieldEvaluationPublisher');
|
|
160
209
|
}
|
|
161
210
|
|
|
162
211
|
private attachContext<T extends ComputedFieldMetadata<any>>(computedField: T, eventContext?: any): T {
|
|
@@ -175,6 +224,10 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface
|
|
|
175
224
|
const base: TypeOrmEventContext = {
|
|
176
225
|
metadataName: event.metadata?.name,
|
|
177
226
|
eventType: eventType,
|
|
227
|
+
// Only pass the transaction's EntityManager for embedded DB (PGlite),
|
|
228
|
+
// where a second pooled connection mid-transaction would deadlock.
|
|
229
|
+
// On regular Postgres, providers use their injected EntityManager.
|
|
230
|
+
...(isEmbeddedDb() ? { manager: event.manager } : {}),
|
|
178
231
|
};
|
|
179
232
|
if ("entityId" in event && event.entityId) {
|
|
180
233
|
base.entityId = event.entityId;
|
|
@@ -38,6 +38,9 @@ export class CreatedByUpdatedBySubscriber implements EntitySubscriberInterface {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// NOTE: If re-enabling loadUser below, query via `event.queryRunner.manager`, NOT
|
|
42
|
+
// `this.defaultDataSource.getRepository(...)`. A second connection opened mid-transaction
|
|
43
|
+
// deadlocks single-threaded engines (PGlite).
|
|
41
44
|
// const loadedUser = await this.loadUser(activeUserOrUndefined as unknown as ActiveUserData);
|
|
42
45
|
// if (isInsert) {
|
|
43
46
|
// event.entity.createdBy = loadedUser?.id;
|
|
@@ -21,6 +21,9 @@ export class FieldMetadataSubscriber implements EntitySubscriberInterface<FieldM
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async afterUpdate(event: UpdateEvent<FieldMetadata>): Promise<void> {
|
|
24
|
+
// NOTE: If re-enabling the block below, use `event.queryRunner.manager` (the active
|
|
25
|
+
// transaction's connection), NOT `this.dataSource.getRepository(...)`. A second
|
|
26
|
+
// connection opened here mid-transaction deadlocks single-threaded engines (PGlite).
|
|
24
27
|
// If a field of type relation.many-to-one is marked for removal, and this field had an inverse relation created
|
|
25
28
|
// It means there is a field of type relation.one-to-many in the co-model, which needs to be removed
|
|
26
29
|
// if (event.entity && event.entity.isMarkedForRemoval && event.entity.type === 'relation' && event.entity.relationType === 'many-to-one' && event.entity.relationCreateInverse === true) {
|
|
@@ -4,6 +4,7 @@ import * as fs from "fs/promises";
|
|
|
4
4
|
import { CreateScheduledJobDto } from "src/dtos/create-scheduled-job.dto";
|
|
5
5
|
import { ModuleMetadata } from "src/entities/module-metadata.entity";
|
|
6
6
|
import { ScheduledJob } from "src/entities/scheduled-job.entity";
|
|
7
|
+
import { isEmbeddedDb } from "src/helpers/environment.helper";
|
|
7
8
|
import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service";
|
|
8
9
|
import { ScheduledJobRepository } from "src/repository/scheduled-job.repository";
|
|
9
10
|
import {
|
|
@@ -83,7 +84,7 @@ export class ScheduledJobSubscriber
|
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const moduleMetadataRepo = event.queryRunner.manager.getRepository(ModuleMetadata);
|
|
87
|
+
const moduleMetadataRepo = (isEmbeddedDb() ? event.queryRunner.manager : this.dataSource).getRepository(ModuleMetadata);
|
|
87
88
|
const populatedModuleMetadata = await moduleMetadataRepo.findOne({
|
|
88
89
|
where: { id: moduleMetadata.id },
|
|
89
90
|
});
|
|
@@ -160,7 +161,7 @@ export class ScheduledJobSubscriber
|
|
|
160
161
|
(job) => job.scheduleName === jobName
|
|
161
162
|
);
|
|
162
163
|
// Insert or update job in metadata
|
|
163
|
-
const jobDto: CreateScheduledJobDto = await this.scheduledJobRepo.toDto(populatedScheduleJob as ScheduledJob);
|
|
164
|
+
const jobDto: CreateScheduledJobDto = await this.scheduledJobRepo.toDto(populatedScheduleJob as ScheduledJob, isEmbeddedDb() ? entityManager : undefined);
|
|
164
165
|
// Strip out bookkeeping/runtime fields before persisting to metadata JSON.
|
|
165
166
|
const {
|
|
166
167
|
moduleId,
|