@solidstarters/solid-core 1.2.123 → 1.2.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/decorators/disallow-in-production.decorator.d.ts +6 -0
- package/dist/decorators/disallow-in-production.decorator.d.ts.map +1 -0
- package/dist/decorators/disallow-in-production.decorator.js +25 -0
- package/dist/decorators/disallow-in-production.decorator.js.map +1 -0
- package/dist/dtos/create-field-metadata.dto.d.ts +14 -0
- package/dist/dtos/create-field-metadata.dto.d.ts.map +1 -1
- package/dist/dtos/create-field-metadata.dto.js +15 -2
- package/dist/dtos/create-field-metadata.dto.js.map +1 -1
- package/dist/entities/field-metadata.entity.d.ts +2 -0
- package/dist/entities/field-metadata.entity.d.ts.map +1 -1
- package/dist/entities/field-metadata.entity.js +5 -1
- package/dist/entities/field-metadata.entity.js.map +1 -1
- package/dist/helpers/solid-registry.d.ts +15 -3
- package/dist/helpers/solid-registry.d.ts.map +1 -1
- package/dist/helpers/solid-registry.js +14 -5
- package/dist/helpers/solid-registry.js.map +1 -1
- package/dist/interfaces.d.ts +11 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/jobs/database/computed-field-evaluation-publisher.service.d.ts +12 -0
- package/dist/jobs/database/computed-field-evaluation-publisher.service.d.ts.map +1 -0
- package/dist/jobs/database/computed-field-evaluation-publisher.service.js +39 -0
- package/dist/jobs/database/computed-field-evaluation-publisher.service.js.map +1 -0
- package/dist/jobs/database/computed-field-evaluation-queue-options.d.ts +8 -0
- package/dist/jobs/database/computed-field-evaluation-queue-options.d.ts.map +1 -0
- package/dist/jobs/database/computed-field-evaluation-queue-options.js +10 -0
- package/dist/jobs/database/computed-field-evaluation-queue-options.js.map +1 -0
- package/dist/jobs/database/computed-field-evaluation-subscriber.service.d.ts +16 -0
- package/dist/jobs/database/computed-field-evaluation-subscriber.service.d.ts.map +1 -0
- package/dist/jobs/database/computed-field-evaluation-subscriber.service.js +48 -0
- package/dist/jobs/database/computed-field-evaluation-subscriber.service.js.map +1 -0
- package/dist/repository/field-metadata.repository.d.ts +8 -0
- package/dist/repository/field-metadata.repository.d.ts.map +1 -0
- package/dist/repository/field-metadata.repository.js +38 -0
- package/dist/repository/field-metadata.repository.js.map +1 -0
- package/dist/repository/field.repository.d.ts +1 -1
- package/dist/repository/field.repository.d.ts.map +1 -1
- package/dist/repository/field.repository.js +6 -6
- package/dist/repository/field.repository.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +14 -1
- package/dist/seeders/system-fields-seeder.service.d.ts +3 -3
- package/dist/seeders/system-fields-seeder.service.d.ts.map +1 -1
- package/dist/seeders/system-fields-seeder.service.js +5 -6
- package/dist/seeders/system-fields-seeder.service.js.map +1 -1
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +14 -0
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -0
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +45 -0
- package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -0
- 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 +9 -1
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/field-metadata.service.d.ts +6 -2
- package/dist/services/field-metadata.service.d.ts.map +1 -1
- package/dist/services/field-metadata.service.js +21 -4
- package/dist/services/field-metadata.service.js.map +1 -1
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +7 -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 +7 -0
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/view-metadata.service.js +1 -2
- 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 +64 -54
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/computed-entity-field.subscriber.d.ts +23 -0
- package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -0
- package/dist/subscribers/computed-entity-field.subscriber.js +95 -0
- package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/# computed field pending issues.md +3 -0
- package/src/decorators/disallow-in-production.decorator.ts +28 -0
- package/src/dtos/create-field-metadata.dto.ts +18 -0
- package/src/entities/field-metadata.entity.ts +4 -0
- package/src/helpers/solid-registry.ts +46 -13
- package/src/interfaces.ts +18 -0
- package/src/jobs/database/computed-field-evaluation-publisher.service.ts +23 -0
- package/src/jobs/database/computed-field-evaluation-queue-options.ts +9 -0
- package/src/jobs/database/computed-field-evaluation-subscriber.service.ts +36 -0
- package/src/repository/field-metadata.repository.ts +26 -0
- package/src/repository/field.repository.ts +5 -2
- package/src/seeders/seed-data/solid-core-metadata.json +14 -1
- package/src/seeders/system-fields-seeder.service.ts +3 -4
- package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +52 -0
- package/src/services/crud.service.ts +9 -1
- package/src/services/field-metadata.service.ts +38 -8
- package/src/services/model-metadata.service.ts +2 -0
- package/src/services/module-metadata.service.ts +2 -0
- package/src/services/view-metadata.service.ts +2 -2
- package/src/solid-core.module.ts +64 -54
- package/src/subscribers/computed-entity-field.subscriber.ts +108 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidstarters/solid-core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.125",
|
|
4
4
|
"description": "This module is a NestJS module containing all the required core providers required by a Solid application",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export enum Environment {
|
|
2
|
+
Production = 'prod',
|
|
3
|
+
Development = 'dev',
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function DisallowInProduction(action?: string) {
|
|
7
|
+
return function (
|
|
8
|
+
target: any,
|
|
9
|
+
propertyKey: string,
|
|
10
|
+
descriptor: PropertyDescriptor,
|
|
11
|
+
) {
|
|
12
|
+
const originalMethod = descriptor.value;
|
|
13
|
+
|
|
14
|
+
descriptor.value = function (...args: any[]) {
|
|
15
|
+
const currentEnv = process.env.ENV as Environment;
|
|
16
|
+
|
|
17
|
+
if (currentEnv === Environment.Production) {
|
|
18
|
+
const className = target.constructor.name;
|
|
19
|
+
const methodName = action || propertyKey;
|
|
20
|
+
throw new Error(`❌ Cannot execute "${className}.${methodName}" in production`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return originalMethod.apply(this, args);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return descriptor;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -171,6 +171,21 @@ export enum ComputedFieldValueType {
|
|
|
171
171
|
datetime = 'datetime',
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
export enum ComputedFieldTriggerOperation {
|
|
175
|
+
beforeInsert = "before-insert",
|
|
176
|
+
afterInsert = "after-insert",
|
|
177
|
+
beforeUpdate = "before-update",
|
|
178
|
+
afterUpdate = "after-update",
|
|
179
|
+
beforeRemove = "before-delete",
|
|
180
|
+
afterRemove = "after-delete",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface ComputedFieldTriggerConfig {
|
|
184
|
+
moduleName: string; // Name of the module where the computed field is defined
|
|
185
|
+
modelName: string;
|
|
186
|
+
operations: ComputedFieldTriggerOperation[]; // List of operations where this computed field should be triggered
|
|
187
|
+
}
|
|
188
|
+
|
|
174
189
|
export class CreateFieldMetadataDto {
|
|
175
190
|
@ApiProperty({ description: 'Name of your field, this is also the name of the column or attribute', })
|
|
176
191
|
// @Matches(/[a-z]+(_[a-z]+)*/, { message: 'Field name should follow snake case conventions only with all lower case.', })
|
|
@@ -370,6 +385,9 @@ export class CreateFieldMetadataDto {
|
|
|
370
385
|
@IsOptional()
|
|
371
386
|
computedFieldValueType: ComputedFieldValueType;
|
|
372
387
|
|
|
388
|
+
@IsOptional()
|
|
389
|
+
computedFieldTriggerConfig: ComputedFieldTriggerConfig[];
|
|
390
|
+
|
|
373
391
|
@IsOptional()
|
|
374
392
|
@ApiProperty({ description: 'Uuid.', })
|
|
375
393
|
@IsString()
|
|
@@ -2,6 +2,7 @@ import { CommonEntity } from "src/entities/common.entity";
|
|
|
2
2
|
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
|
3
3
|
import { MediaStorageProviderMetadata } from "./media-storage-provider-metadata.entity";
|
|
4
4
|
import { ModelMetadata } from "./model-metadata.entity";
|
|
5
|
+
import { ComputedFieldTriggerConfig } from "src/dtos/create-field-metadata.dto";
|
|
5
6
|
|
|
6
7
|
@Entity("ss_field_metadata")
|
|
7
8
|
export class FieldMetadata extends CommonEntity {
|
|
@@ -132,6 +133,9 @@ export class FieldMetadata extends CommonEntity {
|
|
|
132
133
|
@Column({ name: 'computed_field_value_type', nullable: true })
|
|
133
134
|
computedFieldValueType: string;
|
|
134
135
|
|
|
136
|
+
@Column({ name: 'computed_field_trigger_config', nullable: true, type: 'jsonb' })
|
|
137
|
+
computedFieldTriggerConfig: ComputedFieldTriggerConfig[];
|
|
138
|
+
|
|
135
139
|
@Column({ name: 'uuid', nullable: true })
|
|
136
140
|
uuid: string;
|
|
137
141
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
|
|
3
|
+
import { ComputedFieldTriggerConfig, ComputedFieldValueType } from 'src/dtos/create-field-metadata.dto';
|
|
3
4
|
import { CommonEntity } from 'src/entities/common.entity';
|
|
5
|
+
import { Locale } from 'src/entities/locale.entity';
|
|
4
6
|
import { SecurityRule } from 'src/entities/security-rule.entity';
|
|
5
7
|
import { EntityManager } from 'typeorm';
|
|
6
8
|
import { ISelectionProvider, ISelectionProviderContext } from "../interfaces";
|
|
7
|
-
import { Locale } from 'src/entities/locale.entity';
|
|
8
9
|
|
|
9
10
|
type ControllerMetadata = {
|
|
10
11
|
name: string;
|
|
@@ -38,6 +39,18 @@ export enum RESERVED_SOLID_KEYWORDS {
|
|
|
38
39
|
locale = "locale"
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
export interface ComputedFieldMetadata<TContext = any> {
|
|
43
|
+
moduleName: string; // Name of the module where the computed field is defined
|
|
44
|
+
modelName: string; // Name of the model where the computed field is defined
|
|
45
|
+
fieldName: string; // Name of the field that is computed
|
|
46
|
+
computedFieldValueType: ComputedFieldValueType; // Type of the computed field value (e.g., string, number, etc.)
|
|
47
|
+
computedFieldTriggerConfig: ComputedFieldTriggerConfig[]; // JSON stringified object containing the trigger configuration
|
|
48
|
+
// Example: '{"models": ["User", "Product"], "operations": ["create", "update"]}'
|
|
49
|
+
computedFieldValueProviderName: string; // Name of the provider that computes the field value
|
|
50
|
+
// Example: '{"contextKey": "contextValue"}'
|
|
51
|
+
computedFieldValueProviderCtxt: TContext; // Context for the computed field value
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
@Injectable()
|
|
42
55
|
export class SolidRegistry {
|
|
43
56
|
private seeders: Set<InstanceWrapper> = new Set();
|
|
@@ -47,7 +60,8 @@ export class SolidRegistry {
|
|
|
47
60
|
private controllers: Set<ControllerMetadata> = new Set();
|
|
48
61
|
private modules: Set<InstanceWrapper> = new Set();
|
|
49
62
|
private securityRules: SecurityRule[] = [];
|
|
50
|
-
private locales
|
|
63
|
+
private locales: Locale[] = [];
|
|
64
|
+
private computedFieldMetadata: ComputedFieldMetadata[] = [];
|
|
51
65
|
|
|
52
66
|
registerController(name: string, methodNames: string[]): void {
|
|
53
67
|
this.controllers.add({ name: name, methods: methodNames });
|
|
@@ -104,6 +118,14 @@ export class SolidRegistry {
|
|
|
104
118
|
return Array.from(this.computedFieldProviders);
|
|
105
119
|
}
|
|
106
120
|
|
|
121
|
+
getComputedFieldProvider(name: string): InstanceWrapper {
|
|
122
|
+
const provider = this.getComputedFieldProviders().filter((provider) => provider.name === name).pop();
|
|
123
|
+
if (!provider) {
|
|
124
|
+
throw new Error(`Computed Field Provider with name ${name} not found`);
|
|
125
|
+
}
|
|
126
|
+
return provider
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
getSolidDatabaseModules(): Array<InstanceWrapper> {
|
|
108
130
|
return Array.from(this.solidDatabaseModules);
|
|
109
131
|
}
|
|
@@ -121,10 +143,18 @@ export class SolidRegistry {
|
|
|
121
143
|
this.securityRules = securityRules;
|
|
122
144
|
}
|
|
123
145
|
|
|
124
|
-
registerlocales(locales
|
|
146
|
+
registerlocales(locales: Locale[]) {
|
|
125
147
|
this.locales = locales;
|
|
126
148
|
}
|
|
127
|
-
|
|
149
|
+
|
|
150
|
+
registerComputedFieldMetadata(computedFieldMetadata: ComputedFieldMetadata[]) {
|
|
151
|
+
this.computedFieldMetadata = computedFieldMetadata;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getComputedFieldMetadata(): ComputedFieldMetadata[] {
|
|
155
|
+
return this.computedFieldMetadata;
|
|
156
|
+
}
|
|
157
|
+
|
|
128
158
|
//TODO:getlocales from locale model and return default locale where isDefault:true
|
|
129
159
|
getDefaultLocale(): Locale | null {
|
|
130
160
|
return this.locales.find(locale => locale.isDefault === true) || null;
|
|
@@ -141,16 +171,19 @@ export class SolidRegistry {
|
|
|
141
171
|
});
|
|
142
172
|
}
|
|
143
173
|
|
|
174
|
+
/**
|
|
175
|
+
* @deprecated You can directly pass the entityName to typeORM, if you are using this just for doing operations against a particular entity.
|
|
176
|
+
*/
|
|
144
177
|
// Returns the entity target class from the entity name
|
|
145
|
-
getEntityTarget(entityManager: EntityManager, entityName: string): any { //TODO Can be refactored to use this function from crud helper service
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
getCommonEntityKeys(): (keyof CommonEntity)
|
|
152
|
-
return [
|
|
153
|
-
|
|
178
|
+
// getEntityTarget(entityManager: EntityManager, entityName: string): any { //TODO Can be refactored to use this function from crud helper service
|
|
179
|
+
// const entityMetadatas = entityManager.connection.entityMetadatas;
|
|
180
|
+
// const entityMetadata = entityMetadatas.find(em => em.name === entityName);
|
|
181
|
+
// return entityMetadata.target;
|
|
182
|
+
// }
|
|
183
|
+
|
|
184
|
+
getCommonEntityKeys(): (keyof CommonEntity)[] {
|
|
185
|
+
return ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'deletedTracker', 'localeName', 'defaultEntityLocaleId', 'publishedAt'];
|
|
186
|
+
// return Reflect.getMetadataKeys(CommonEntity.prototype) as (keyof CommonEntity)[];
|
|
154
187
|
}
|
|
155
188
|
|
|
156
189
|
}
|
package/src/interfaces.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { CreateViewMetadataDto } from './dtos/create-view-metadata.dto';
|
|
|
12
12
|
import { FieldMetadata } from './entities/field-metadata.entity';
|
|
13
13
|
import { Media } from './entities/media.entity';
|
|
14
14
|
import { Readable } from 'stream';
|
|
15
|
+
import { ComputedFieldMetadata } from './helpers/solid-registry';
|
|
15
16
|
|
|
16
17
|
export interface FieldCrudManager {
|
|
17
18
|
// fieldMetadata: FieldMetadata;
|
|
@@ -80,6 +81,9 @@ export interface ISelectionProvider<T extends ISelectionProviderContext> {
|
|
|
80
81
|
values(query: any, ctxt: T): Promise<readonly ISelectionProviderValues[]>;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated Use `IEntityComputedFieldProvider` instead.
|
|
86
|
+
*/
|
|
83
87
|
export interface IComputedFieldProvider<T> {
|
|
84
88
|
help(): string;
|
|
85
89
|
|
|
@@ -90,6 +94,20 @@ export interface IComputedFieldProvider<T> {
|
|
|
90
94
|
computeValue(dto: any, ctxt: T): Promise<string | number>; // FIXME : Improve the types to make it more specific using generics
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
export interface IEntityComputedFieldProvider {
|
|
98
|
+
help(): string;
|
|
99
|
+
|
|
100
|
+
name(): string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface IEntityPreComputeFieldProvider<TTriggerEntity, TContext, TValue> extends IEntityComputedFieldProvider {
|
|
104
|
+
preComputeValue(entity: TTriggerEntity, computedFieldMetadata: ComputedFieldMetadata<TContext>): Promise<TValue>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface IEntityPostComputeFieldProvider<TTriggerEntity, TContext> extends IEntityComputedFieldProvider {
|
|
108
|
+
postComputeAndSaveValue(triggerEntity: TTriggerEntity, computedFieldMetadata: ComputedFieldMetadata<TContext>): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
export interface ISolidDatabaseModule {
|
|
94
112
|
name(): string;
|
|
95
113
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import { QueuesModuleOptions } from "src/interfaces";
|
|
3
|
+
import { MqMessageQueueService } from "src/services/mq-message-queue.service";
|
|
4
|
+
import { MqMessageService } from "src/services/mq-message.service";
|
|
5
|
+
import { DatabasePublisher } from "src/services/queues/database-publisher.service";
|
|
6
|
+
import { ComputedFieldEvaluationPayload } from "src/subscribers/computed-entity-field.subscriber";
|
|
7
|
+
import computedFieldEvaluationQueueOptions from "./computed-field-evaluation-queue-options";
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class ComputedFieldEvaluationPublisher extends DatabasePublisher<ComputedFieldEvaluationPayload> {
|
|
11
|
+
constructor(
|
|
12
|
+
protected readonly mqMessageService: MqMessageService,
|
|
13
|
+
protected readonly mqMessageQueueService: MqMessageQueueService,
|
|
14
|
+
) {
|
|
15
|
+
super(mqMessageService, mqMessageQueueService);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
options(): QueuesModuleOptions {
|
|
19
|
+
return {
|
|
20
|
+
...computedFieldEvaluationQueueOptions
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import { SolidRegistry } from "src/helpers/solid-registry";
|
|
3
|
+
import { IEntityPostComputeFieldProvider, IEntityComputedFieldProvider, QueuesModuleOptions } from "src/interfaces";
|
|
4
|
+
import { QueueMessage } from "src/interfaces/mq";
|
|
5
|
+
import { MqMessageQueueService } from "src/services/mq-message-queue.service";
|
|
6
|
+
import { MqMessageService } from "src/services/mq-message.service";
|
|
7
|
+
import { DatabaseSubscriber } from "src/services/queues/database-subscriber.service";
|
|
8
|
+
import { ComputedFieldEvaluationPayload } from "src/subscribers/computed-entity-field.subscriber";
|
|
9
|
+
import computedFieldEvaluationQueueOptions from "./computed-field-evaluation-queue-options";
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ComputedFieldEvaluationSubscriber extends DatabaseSubscriber<ComputedFieldEvaluationPayload> {
|
|
13
|
+
constructor(
|
|
14
|
+
readonly mqMessageService: MqMessageService,
|
|
15
|
+
readonly mqMessageQueueService: MqMessageQueueService,
|
|
16
|
+
readonly solidRegistry: SolidRegistry
|
|
17
|
+
) {
|
|
18
|
+
super(mqMessageService, mqMessageQueueService);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
options(): QueuesModuleOptions {
|
|
22
|
+
return {
|
|
23
|
+
...computedFieldEvaluationQueueOptions
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// This method will use the ComputedFieldEvaluationPayload to evaluate the computed fields
|
|
28
|
+
// It will then call the corresponding provider computeAndSave method to perform the evaluation
|
|
29
|
+
async subscribe(message: QueueMessage<ComputedFieldEvaluationPayload>) {
|
|
30
|
+
const { databaseEntity, ...computedFieldMetadata } = message.payload;
|
|
31
|
+
const provider = this.solidRegistry.getComputedFieldProvider(computedFieldMetadata.computedFieldValueProviderName);
|
|
32
|
+
// Get the instance of the provider and assert it is of type IEntityComputedFieldProvider
|
|
33
|
+
const providerInstance = provider.instance as IEntityPostComputeFieldProvider<any, any>; // IEntityComputedFieldProvider
|
|
34
|
+
await providerInstance.postComputeAndSaveValue(databaseEntity, computedFieldMetadata); //FIXME There should some way to check/assert if the provider actually has a postComputeAndSaveValue
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { SolidFieldType } from 'src/dtos/create-field-metadata.dto';
|
|
3
|
+
import { FieldMetadata } from 'src/entities/field-metadata.entity';
|
|
4
|
+
import { DataSource, Repository } from 'typeorm';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class FieldMetadataRepository extends Repository<FieldMetadata> {
|
|
8
|
+
private readonly logger = new Logger(FieldMetadataRepository.name);
|
|
9
|
+
constructor(
|
|
10
|
+
dataSource: DataSource,
|
|
11
|
+
) {
|
|
12
|
+
super(FieldMetadata, dataSource.createEntityManager());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async findComputedFieldsPopulatedWithModelAndModule() {
|
|
16
|
+
const computedFields = await this.find({
|
|
17
|
+
where: {
|
|
18
|
+
type: SolidFieldType.computed,
|
|
19
|
+
},
|
|
20
|
+
relations: ['model', 'model.module'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return computedFields;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { Repository, DataSource } from 'typeorm';
|
|
2
1
|
import { Injectable } from '@nestjs/common';
|
|
3
|
-
import { FieldMetadata } from 'src/entities/field-metadata.entity';
|
|
4
2
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
3
|
+
import { FieldMetadata } from 'src/entities/field-metadata.entity';
|
|
4
|
+
import { DataSource, Repository } from 'typeorm';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Use `FieldMetadataRepository` instead.
|
|
8
|
+
*/
|
|
6
9
|
@Injectable()
|
|
7
10
|
export class FieldRepository extends Repository<FieldMetadata> {
|
|
8
11
|
constructor(
|
|
@@ -844,6 +844,19 @@
|
|
|
844
844
|
"columnName": "computed_field_value_type",
|
|
845
845
|
"isSystem": true
|
|
846
846
|
},
|
|
847
|
+
{
|
|
848
|
+
"name": "computedFieldTriggerConfig",
|
|
849
|
+
"displayName": "Computed Field Value Trigger Configuration",
|
|
850
|
+
"type": "json",
|
|
851
|
+
"ormType": "text",
|
|
852
|
+
"required": false,
|
|
853
|
+
"unique": false,
|
|
854
|
+
"index": false,
|
|
855
|
+
"private": false,
|
|
856
|
+
"encrypt": false,
|
|
857
|
+
"columnName": "computed_field_trigger_config",
|
|
858
|
+
"isSystem": true
|
|
859
|
+
},
|
|
847
860
|
{
|
|
848
861
|
"name": "uuid",
|
|
849
862
|
"displayName": "UUID",
|
|
@@ -2609,7 +2622,7 @@
|
|
|
2609
2622
|
"ormType": "integer",
|
|
2610
2623
|
"isSystem": false,
|
|
2611
2624
|
"relationType": "many-to-one",
|
|
2612
|
-
"relationCoModelFieldName": "
|
|
2625
|
+
"relationCoModelFieldName": "savedFilters",
|
|
2613
2626
|
"relationCreateInverse": false,
|
|
2614
2627
|
"relationCoModelSingularName": "user",
|
|
2615
2628
|
"relationCoModelColumnName": null,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { InjectRepository } from "@nestjs/typeorm";
|
|
5
5
|
import { ModelMetadata } from "src/entities/model-metadata.entity";
|
|
6
6
|
import { ModelMetadataHelperService } from "src/helpers/model-metadata-helper.service";
|
|
7
|
-
import {
|
|
7
|
+
import { FieldMetadataRepository } from "src/repository/field-metadata.repository";
|
|
8
8
|
import { Repository } from "typeorm";
|
|
9
9
|
|
|
10
10
|
export class SystemFieldsSeederService {
|
|
@@ -15,8 +15,7 @@ export class SystemFieldsSeederService {
|
|
|
15
15
|
private readonly modelHelperService: ModelMetadataHelperService,
|
|
16
16
|
@InjectRepository(ModelMetadata)
|
|
17
17
|
private readonly modelRepository: Repository<ModelMetadata>, // Replace with actual model repository type
|
|
18
|
-
|
|
19
|
-
private readonly fieldRepository: Repository<FieldRepository>, // Replace with actual field repository type
|
|
18
|
+
private readonly fieldMetadataRepository: FieldMetadataRepository, // Replace with actual field repository type
|
|
20
19
|
) {}
|
|
21
20
|
|
|
22
21
|
async seed() {
|
|
@@ -46,7 +45,7 @@ export class SystemFieldsSeederService {
|
|
|
46
45
|
...field,
|
|
47
46
|
model: model, // Associate the field with the current model
|
|
48
47
|
}));
|
|
49
|
-
await this.
|
|
48
|
+
await this.fieldMetadataRepository.save(newFields);
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import { kebabCase } from "lodash";
|
|
3
|
+
import { ComputedFieldProvider } from "src/decorators/computed-field-provider.decorator";
|
|
4
|
+
import { CommonEntity } from "src/entities/common.entity";
|
|
5
|
+
import { ComputedFieldMetadata } from "src/helpers/solid-registry";
|
|
6
|
+
import { IEntityPreComputeFieldProvider } from "src/interfaces";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export interface ConcatComputedFieldContext {
|
|
10
|
+
separator: string; // The separator to use between concatenated values
|
|
11
|
+
fields: string[]; // The fields to concatenate
|
|
12
|
+
slugify?: boolean; // Optional: if true, slugify each field value before concatenation
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@ComputedFieldProvider()
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class ConcatEntityComputedFieldProvider<T extends CommonEntity> implements IEntityPreComputeFieldProvider<T, ConcatComputedFieldContext, string> {
|
|
18
|
+
|
|
19
|
+
name(): string {
|
|
20
|
+
return "ConcatEntityComputedFieldProvider";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
help(): string {
|
|
24
|
+
return "Computed field provider used to create fields whose value is a concatenation of other fields in the same model.";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async preComputeValue(entity: T, computedFieldMetadata: ComputedFieldMetadata<ConcatComputedFieldContext>): Promise<string> {
|
|
28
|
+
const { computedFieldValueProviderCtxt } = computedFieldMetadata;
|
|
29
|
+
const separator = computedFieldValueProviderCtxt.separator || ' '; // Default to space if no separator is provided
|
|
30
|
+
const fields = computedFieldValueProviderCtxt.fields || [];
|
|
31
|
+
const slugify = computedFieldValueProviderCtxt.slugify || false;
|
|
32
|
+
|
|
33
|
+
let concatenatedString = '';
|
|
34
|
+
for (let i = 0; i < fields.length; i++) {
|
|
35
|
+
const field = fields[i];
|
|
36
|
+
|
|
37
|
+
// if slugify then each field val to be converted to a slug before concatenation
|
|
38
|
+
let fieldVal = entity[field];
|
|
39
|
+
if (slugify && typeof fieldVal === 'string') {
|
|
40
|
+
fieldVal = kebabCase(fieldVal);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (concatenatedString) {
|
|
44
|
+
concatenatedString += separator;
|
|
45
|
+
}
|
|
46
|
+
concatenatedString += fieldVal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return concatenatedString;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
@@ -403,7 +403,7 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
|
|
|
403
403
|
|
|
404
404
|
// The value will be computed by the computed provider
|
|
405
405
|
// Invoke the appropriate computed provider, get the value and add to the dto
|
|
406
|
-
const options = { ...commonOptions, computedFieldProvider: fieldMetadata.computedFieldValueProvider, computedFieldValueProviderCtxt: fieldMetadata.computedFieldValueProviderCtxt, computedFieldValueType: fieldMetadata.computedFieldValueType as ComputedFieldValueType, discoveryService: this.discoveryService, skipComputation: isPartialUpdate };
|
|
406
|
+
const options = { ...commonOptions, computedFieldProvider: fieldMetadata.computedFieldValueProvider, computedFieldValueProviderCtxt: fieldMetadata.computedFieldValueProviderCtxt, computedFieldValueType: fieldMetadata.computedFieldValueType as ComputedFieldValueType, discoveryService: this.discoveryService, skipComputation: this.isSkipComputation(isPartialUpdate, fieldMetadata) };
|
|
407
407
|
return new ComputedFieldCrudManager(options);
|
|
408
408
|
}
|
|
409
409
|
default:
|
|
@@ -411,6 +411,14 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
|
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
private isSkipComputation(isPartialUpdate: boolean, computedFieldMetadata: FieldMetadata) {
|
|
415
|
+
if (isPartialUpdate) return true; // If it is a partial update, then skip computation
|
|
416
|
+
if (computedFieldMetadata.computedFieldTriggerConfig && computedFieldMetadata.computedFieldTriggerConfig.length > 0) {
|
|
417
|
+
return true; // computedFieldTriggerConfig is a new field introduced as part of the IEntityComputedFieldProvider new interface, so this computation will be skiipped in crud service & will be called in the subscriber instead
|
|
418
|
+
}
|
|
419
|
+
return false; // If it is not a partial update, then do not skip computation
|
|
420
|
+
}
|
|
421
|
+
|
|
414
422
|
async find(basicFilterDto: BasicFilterDto, solidRequestContext: any = {}) {
|
|
415
423
|
const alias = 'entity';
|
|
416
424
|
// Extract the required keys from the input query
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { classify } from '@angular-devkit/core/src/utils/strings';
|
|
2
|
+
import { Injectable, Logger, NotFoundException, OnApplicationBootstrap } from '@nestjs/common';
|
|
3
|
+
import { InjectDataSource } from '@nestjs/typeorm';
|
|
3
4
|
import * as fs from 'fs/promises'; // Use the Promise-based version of fs for async/await
|
|
4
5
|
import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
|
|
5
|
-
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
6
|
+
import { ComputedFieldMetadata, SolidRegistry } from 'src/helpers/solid-registry';
|
|
7
|
+
import { FieldMetadataRepository } from 'src/repository/field-metadata.repository';
|
|
6
8
|
import { DataSource, Repository, SelectQueryBuilder } from 'typeorm';
|
|
7
9
|
import { BasicFilterDto } from '../dtos/basic-filters.dto';
|
|
8
|
-
import { CascadeType, ComputedFieldValueType, CreateFieldMetadataDto, DecryptWhenType, EncryptionType, MediaType, PSQLType, RelationType, SelectionValueType, SolidFieldType } from '../dtos/create-field-metadata.dto';
|
|
10
|
+
import { CascadeType, ComputedFieldTriggerConfig, ComputedFieldTriggerOperation, ComputedFieldValueType, CreateFieldMetadataDto, DecryptWhenType, EncryptionType, MediaType, PSQLType, RelationType, SelectionValueType, SolidFieldType } from '../dtos/create-field-metadata.dto';
|
|
9
11
|
import { SelectionDynamicQueryDto } from '../dtos/selection-dynamic-query.dto';
|
|
10
12
|
import { UpdateFieldMetaDataDto } from '../dtos/update-field-metadata.dto';
|
|
11
13
|
import { FieldMetadata } from '../entities/field-metadata.entity';
|
|
12
14
|
import { ModelMetadata } from '../entities/model-metadata.entity';
|
|
13
15
|
import { ISelectionProviderValues } from '../interfaces';
|
|
14
16
|
import { CrudHelperService } from './crud-helper.service';
|
|
15
|
-
import { classify } from '@angular-devkit/core/src/utils/strings';
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@Injectable()
|
|
19
|
-
export class FieldMetadataService {
|
|
20
|
+
export class FieldMetadataService implements OnApplicationBootstrap {
|
|
20
21
|
constructor(
|
|
21
|
-
|
|
22
|
-
private readonly fieldMetadataRepo: Repository<FieldMetadata>,
|
|
22
|
+
private readonly fieldMetadataRepo: FieldMetadataRepository,
|
|
23
23
|
@InjectDataSource()
|
|
24
24
|
private readonly dataSource: DataSource,
|
|
25
25
|
private readonly solidRegistry: SolidRegistry,
|
|
@@ -29,6 +29,36 @@ export class FieldMetadataService {
|
|
|
29
29
|
|
|
30
30
|
private logger = new Logger(FieldMetadataService.name);
|
|
31
31
|
|
|
32
|
+
onApplicationBootstrap() {
|
|
33
|
+
this.loadAndRegisterComputedFieldsDetails();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async loadAndRegisterComputedFieldsDetails() {
|
|
37
|
+
// Load all the modules and models and within that load all the computed fields
|
|
38
|
+
const computedFieldsWithModelAndModule = await this.fieldMetadataRepo.findComputedFieldsPopulatedWithModelAndModule();
|
|
39
|
+
|
|
40
|
+
// Convert the computed fields object above to the ComputedFieldMetadata type
|
|
41
|
+
const computedFieldMetadata: ComputedFieldMetadata[] = computedFieldsWithModelAndModule.map((field) => {
|
|
42
|
+
// const defaultComputedFieldTriggerConfig: ComputedFieldTriggerConfig = {
|
|
43
|
+
// moduleName: field.model.module.name,
|
|
44
|
+
// modelName: field.model.singularName,
|
|
45
|
+
// operations: [ComputedFieldTriggerOperation.create, ComputedFieldTriggerOperation.update, ComputedFieldTriggerOperation.delete], // Default operations, can be overridden
|
|
46
|
+
// }
|
|
47
|
+
return {
|
|
48
|
+
moduleName: field.model.module.name,
|
|
49
|
+
modelName: field.model.singularName,
|
|
50
|
+
fieldName: field.name,
|
|
51
|
+
computedFieldValueType: field.computedFieldValueType as ComputedFieldValueType,
|
|
52
|
+
computedFieldTriggerConfig: field.computedFieldTriggerConfig ?? [], // Ensure it's an array, default to empty if not provided
|
|
53
|
+
computedFieldValueProviderName: field.computedFieldValueProvider,
|
|
54
|
+
computedFieldValueProviderCtxt: field.computedFieldValueProviderCtxt ? JSON.parse(field.computedFieldValueProviderCtxt) : {}, // Parse the context if it exists, default to empty object
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Register the computed fields in the SolidRegistry. Capture only computed field related info
|
|
59
|
+
this.solidRegistry.registerComputedFieldMetadata(computedFieldMetadata);
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
async updateInverseField(field: FieldMetadata, fieldRepository: Repository<FieldMetadata>, modelRepository: Repository<ModelMetadata>) {
|
|
33
63
|
if (!field.model || !field.model.module) {
|
|
34
64
|
throw new Error('Model and module are required to update inverse field');
|
|
@@ -27,6 +27,7 @@ import { RoleMetadataService } from './role-metadata.service';
|
|
|
27
27
|
import { GenerateCodePublisher } from 'src/jobs/database/generate-code-publisher.service';
|
|
28
28
|
import { PermissionMetadata } from 'src/entities/permission-metadata.entity';
|
|
29
29
|
import { classify, dasherize } from '@angular-devkit/core/src/utils/strings';
|
|
30
|
+
import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator';
|
|
30
31
|
|
|
31
32
|
@Injectable()
|
|
32
33
|
export class ModelMetadataService {
|
|
@@ -711,6 +712,7 @@ export class ModelMetadataService {
|
|
|
711
712
|
|
|
712
713
|
}
|
|
713
714
|
|
|
715
|
+
@DisallowInProduction()
|
|
714
716
|
async handleGenerateCode(options: CodeGenerationOptions): Promise<any> {
|
|
715
717
|
// // see if the model record exists.
|
|
716
718
|
// const model = await this.modelMetadataRepo.findOne({
|
|
@@ -21,6 +21,7 @@ import { CodeGenerationOptions, ModuleMetadataConfiguration } from '../interface
|
|
|
21
21
|
import { CrudHelperService } from './crud-helper.service';
|
|
22
22
|
import { ModelMetadataService } from './model-metadata.service';
|
|
23
23
|
import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
|
|
24
|
+
import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator';
|
|
24
25
|
|
|
25
26
|
@Injectable()
|
|
26
27
|
export class ModuleMetadataService {
|
|
@@ -349,6 +350,7 @@ export class ModuleMetadataService {
|
|
|
349
350
|
return true
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
@DisallowInProduction()
|
|
352
354
|
async generateCode(options: CodeGenerationOptions): Promise<string> {
|
|
353
355
|
if (!options.moduleId && !options.moduleUserKey) {
|
|
354
356
|
throw new BadRequestException('Module ID or Module Name is required for generating code');
|
|
@@ -57,8 +57,8 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
|
|
|
57
57
|
defaultEntityLocaleIdFromQuery?: string
|
|
58
58
|
): Promise<{ records: any[], defaultEntityLocaleId: string | null }> {
|
|
59
59
|
const solidRegistry = await this.moduleRef.get(SolidRegistry, { strict: false });
|
|
60
|
-
const currentEntityTarget = solidRegistry.getEntityTarget(this.entityManager, classify(modelName));
|
|
61
|
-
const currentEntityRepository = this.entityManager.getRepository(
|
|
60
|
+
// const currentEntityTarget = solidRegistry.getEntityTarget(this.entityManager, classify(modelName));
|
|
61
|
+
const currentEntityRepository = this.entityManager.getRepository(classify(modelName));
|
|
62
62
|
|
|
63
63
|
// Case 1: Creating a new record with no defaultEntityLocaleId to clone
|
|
64
64
|
if (id === 'new' && !defaultEntityLocaleIdFromQuery) {
|