@solidstarters/solid-core 1.2.36 → 1.2.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidstarters/solid-core",
3
- "version": "1.2.36",
3
+ "version": "1.2.38",
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",
@@ -205,7 +205,8 @@ export class ModuleMetadataSeederService {
205
205
  this.logger.debug(`About to add all permissions to the Admin role`);
206
206
  await this.roleService.addAllPermissionsToRole("Admin");
207
207
  // 2. Give wrapSettings permissions to the Public role.
208
- const internalRolePermission = ['UserController.findMany',
208
+ const internalRolePermission = [
209
+ 'UserController.findMany',
209
210
  'UserController.checkIfPermissionExists',
210
211
  'UserController.findOne',
211
212
  'MenuItemMetadataController.findMany',
@@ -214,7 +215,10 @@ export class ModuleMetadataSeederService {
214
215
  'ViewMetadataController.getLayout',
215
216
  'ViewMetadataController.findMany',
216
217
  'ViewMetadataController.findOne',
217
- 'AuthenticationController.changePassword'
218
+ 'AuthenticationController.changePassword',
219
+ 'FieldMetadataController.getSelectionDynamicValues',
220
+ 'FieldMetadataController.getSelectionDynamicValue',
221
+ 'FieldMetadataController.findFieldDefaultMetaData',
218
222
  ]
219
223
  await this.roleService.addPermissionToRole('Internal User', internalRolePermission);
220
224
  await this.roleService.addPermissionToRole('Public', ['SettingController.wrapSettings']);
@@ -141,7 +141,7 @@ export class CrudHelperService {
141
141
  }, {});
142
142
  }
143
143
 
144
- private normalize(value: string | string[]): string[] {
144
+ normalize(value: string | string[]): string[] {
145
145
  if (!value) return [];// if the value is nullish, then return an empty array
146
146
  return Array.isArray(value) ? value : [value]; // if the value is an array, return it as is, otherwise return it as an array
147
147
  }
@@ -40,7 +40,7 @@ import { SolidRequestContextDto } from "src/dtos/solid-request-context.dto";
40
40
 
41
41
  const DEFAULT_LIMIT = 10;
42
42
  const DEFAULT_OFFSET = 0;
43
- export class CRUDService<T> { //Add two generic value i.e Person,CreatePersonDto, so we get the proper types in our service
43
+ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDto, so we get the proper types in our service
44
44
 
45
45
  constructor(
46
46
  readonly modelMetadataService: ModelMetadataService,
@@ -414,7 +414,6 @@ export class CRUDService<T> { //Add two generic value i.e Person,CreatePersonDto
414
414
  }
415
415
  }
416
416
 
417
-
418
417
  async find(basicFilterDto: BasicFilterDto, solidRequestContext: any = {}) {
419
418
  const alias = 'entity';
420
419
  // Extract the required keys from the input query
@@ -428,13 +427,19 @@ export class CRUDService<T> { //Add two generic value i.e Person,CreatePersonDto
428
427
  }
429
428
  }
430
429
 
430
+ // Exclude one-to-many and many-to-one relations from the initial filter query, since they will be queried separately
431
+ const relationsExcludedFromInitialQuery = this.relationsExcludedFromInitialQuery(model, basicFilterDto.populate);
432
+ basicFilterDto = this.getRevisedFilterDto(basicFilterDto, relationsExcludedFromInitialQuery);
433
+
431
434
  // Create above query on pincode table using query builder
432
435
  var qb: SelectQueryBuilder<T> = this.repo.createQueryBuilder(alias)
433
436
  qb = this.crudHelperService.buildFilterQuery(qb, basicFilterDto, alias);
434
-
437
+
435
438
  if (basicFilterDto.groupBy) {
439
+ const relationsExcludedFromInitialQuery = this.relationsExcludedFromInitialQuery(model, groupFilter.populate);
440
+ groupFilter = this.getRevisedFilterDto(groupFilter, relationsExcludedFromInitialQuery);
436
441
  // Get the records and the count
437
- const { groupMeta, groupRecords } = await this.handleGroupFind(qb, groupFilter, populateGroup, alias, populateMedia);
442
+ const { groupMeta, groupRecords } = await this.handleGroupFind(qb, groupFilter, populateGroup, alias, populateMedia, relationsExcludedFromInitialQuery);
438
443
  return {
439
444
  groupMeta,
440
445
  groupRecords,
@@ -442,7 +447,7 @@ export class CRUDService<T> { //Add two generic value i.e Person,CreatePersonDto
442
447
  }
443
448
  else {
444
449
  // Get the records and the count
445
- const { meta, records } = await this.handleNonGroupFind(qb, populateMedia, offset, limit);
450
+ const { meta, records } = await this.handleNonGroupFind(qb, populateMedia, offset, limit, alias, relationsExcludedFromInitialQuery);
446
451
  return {
447
452
  meta,
448
453
  records,
@@ -450,9 +455,28 @@ export class CRUDService<T> { //Add two generic value i.e Person,CreatePersonDto
450
455
  }
451
456
  }
452
457
 
453
- private async handleNonGroupFind(qb: SelectQueryBuilder<T>, populateMedia: string[], offset: number, limit: number) {
458
+ private getRevisedFilterDto(basicFilterDto: BasicFilterDto, relationsExcludedFromInitialQuery: string[]): BasicFilterDto {
459
+ const normalizedPopulate = this.crudHelperService.normalize(basicFilterDto.populate);
460
+ if (normalizedPopulate.length === 0 || relationsExcludedFromInitialQuery.length === 0) return basicFilterDto;
461
+ return { ...basicFilterDto, populate: normalizedPopulate.filter(populate => !relationsExcludedFromInitialQuery.includes(populate)) };
462
+ }
463
+
464
+ private relationsExcludedFromInitialQuery(model: ModelMetadata, relationsToBePopulated: string[] = []): string[] {
465
+ const relationToBeExcluded =
466
+ model.fields
467
+ .filter(field => field.type === 'relation' && [RelationType.manyTomany, RelationType.oneToMany].includes(field.relationType as RelationType))
468
+ .map(field => field.name);
469
+ return relationsToBePopulated.filter(relation => relationToBeExcluded.includes(relation));
470
+ }
471
+
472
+ private async handleNonGroupFind(qb: SelectQueryBuilder<T>, populateMedia: string[], offset: number, limit: number, alias: string, relationsExcludedFromInitialQuery: string[]) {
454
473
  const [entities, count] = await qb.getManyAndCount();
455
474
 
475
+ // Populate the excluded relations for the entities
476
+ if (relationsExcludedFromInitialQuery.length > 0) {
477
+ await this.populateExcludedRelations(entities, relationsExcludedFromInitialQuery, alias);
478
+ }
479
+
456
480
  // Populate the entity with the media
457
481
  if (populateMedia && populateMedia.length > 0) {
458
482
  await this.handlePopulateMedia(populateMedia, entities);
@@ -461,7 +485,36 @@ export class CRUDService<T> { //Add two generic value i.e Person,CreatePersonDto
461
485
  return this.wrapFindResponse(offset, limit, count, entities);
462
486
  }
463
487
 
464
- private async handleGroupFind(qb: SelectQueryBuilder<T>, groupFilter: BasicFilterDto, populateGroup: boolean, alias: string, populateMedia: string[]) {
488
+ private async populateExcludedRelations(entities: T[], relationsExcludedFromInitialQuery: string[], alias: string) {
489
+ //@ts-ignore
490
+ const ids = entities.map(entity => entity.id);
491
+
492
+ // Fire a query to get the records from the relation entity which match the ids
493
+ // Create a map with key as the entity id and value as the qb records
494
+ const relationEntitiesMap = {};
495
+ for (const relation of relationsExcludedFromInitialQuery) {
496
+ const qb = this.repo.createQueryBuilder(`${alias}`)
497
+ .leftJoinAndSelect(`${alias}.${relation}`, relation)
498
+ .where(`${alias}.id IN (:...ids)`, { ids })
499
+ // .limit(DEFAULT_LIMIT)
500
+ // .offset(DEFAULT_OFFSET);
501
+ const relationEntities = await qb.getMany();
502
+ relationEntitiesMap[relation] = relationEntities;
503
+ }
504
+
505
+ // Iterate over the map and assign the relation entities to the entity
506
+ for (const relation of relationsExcludedFromInitialQuery) {
507
+ for (const entity of entities) {
508
+ const entityRelations = relationEntitiesMap[relation]
509
+ //@ts-ignore
510
+ .filter((joinedEntity: T) => joinedEntity.id === entity.id)
511
+ .flatMap((joinedEntity: T) => joinedEntity[relation]);
512
+ entity[relation] = entityRelations;
513
+ }
514
+ }
515
+ }
516
+
517
+ private async handleGroupFind(qb: SelectQueryBuilder<T>, groupFilter: BasicFilterDto, populateGroup: boolean, alias: string, populateMedia: string[], relationsExcludedFromInitialQuery: string[]) {
465
518
  const groupByResult = await qb.getRawMany();
466
519
 
467
520
  const groupMeta = [];
@@ -470,15 +523,14 @@ export class CRUDService<T> { //Add two generic value i.e Person,CreatePersonDto
470
523
  for (const group of groupByResult) {
471
524
  if (populateGroup) {
472
525
  let groupByQb: SelectQueryBuilder<T> = this.repo.createQueryBuilder(alias);
473
- // For the group by records, apply the basic filter
474
- // const basicFilterDto = {
475
- // limit: DEFAULT_LIMIT,
476
- // offset: DEFAULT_OFFSET,
477
- // };
478
526
  groupByQb = this.crudHelperService.buildFilterQuery(groupByQb, groupFilter, alias);
479
527
  groupByQb = this.crudHelperService.buildGroupByRecordsQuery(groupByQb, group, alias);
480
528
  const [entities, count] = await groupByQb.getManyAndCount();
481
529
 
530
+ // Populate the excluded relations for the entities
531
+ if (relationsExcludedFromInitialQuery.length > 0) {
532
+ await this.populateExcludedRelations(entities, relationsExcludedFromInitialQuery, alias);
533
+ }
482
534
 
483
535
  // Populate the entity with the media
484
536
  if (populateMedia && populateMedia.length > 0) {
@@ -1,3 +1,4 @@
1
+ import { classify } from '@angular-devkit/core/src/utils/strings';
1
2
  import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
2
3
  import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
3
4
  import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
@@ -5,6 +6,7 @@ import { IS_COMPUTED_FIELD_PROVIDER } from 'src/decorators/computed-field-provid
5
6
  import { IS_SELECTION_PROVIDER } from 'src/decorators/selection-provider.decorator';
6
7
  import { IS_SOLID_DATABASE_MODULE } from 'src/decorators/solid-database-module.decorator';
7
8
  import { SolidRegistry } from 'src/helpers/solid-registry';
9
+ import { CRUDService } from './crud.service';
8
10
 
9
11
  @Injectable()
10
12
  export class SolidIntrospectService implements OnApplicationBootstrap {
@@ -16,9 +18,9 @@ export class SolidIntrospectService implements OnApplicationBootstrap {
16
18
  ) { }
17
19
 
18
20
  private readonly logger = new Logger(SolidIntrospectService.name);
19
- onApplicationBootstrap() {
21
+ onApplicationBootstrap() {
20
22
  this.logger.log('Introspecting the application for Solid metadata');
21
-
23
+
22
24
  // Register all seeders
23
25
  const seeders = this.discoveryService
24
26
  .getProviders()
@@ -55,7 +57,7 @@ export class SolidIntrospectService implements OnApplicationBootstrap {
55
57
  .getProviders()
56
58
  .filter((provider) => this.isSolidDatabaseModule(provider));
57
59
 
58
- solidDatabaseModules.forEach((solidDatabaseModule) => {
60
+ solidDatabaseModules.forEach((solidDatabaseModule) => {
59
61
  // @ts-ignore
60
62
  this.solidRegistry.registerSolidDatabaseModule(solidDatabaseModule);
61
63
  });
@@ -128,14 +130,13 @@ export class SolidIntrospectService implements OnApplicationBootstrap {
128
130
  return !!isSolidDatabaseModule;
129
131
  }
130
132
 
131
-
132
133
  private isModule(provider: InstanceWrapper): boolean {
133
134
  const metatype = provider.metatype;
134
135
  // Check if it's a Static Module (Class-Based)
135
136
  if (metatype && typeof metatype === 'function' && Reflect.getMetadata('imports', metatype)) {
136
137
  return true;
137
138
  }
138
-
139
+
139
140
  // Ensure provider.instance is an object before checking for 'module'
140
141
  if (provider.instance && typeof provider.instance === 'object') {
141
142
  // Check if it's a Dynamic Module (Object-Based)
@@ -143,8 +144,21 @@ export class SolidIntrospectService implements OnApplicationBootstrap {
143
144
  return true;
144
145
  }
145
146
  }
146
-
147
+
147
148
  return false;
148
149
  }
149
150
 
151
+ /**
152
+ * Given a model singular name this will return the crud service instance.
153
+ * @param modelSingularName
154
+ * @returns
155
+ */
156
+ getCRUDService(modelSingularName: string): CRUDService<any> {
157
+ const provider = this.getProvider(`${classify(modelSingularName)}Service`);
158
+ return provider?.instance as CRUDService<any>;
159
+ }
160
+
161
+ getProvider(providerName: string) {
162
+ return this.discoveryService.getProviders().find((provider) => provider.name === providerName);
163
+ }
150
164
  }
@@ -1,4 +1,4 @@
1
- import { Injectable } from '@nestjs/common';
1
+ import { BadRequestException, Injectable } from '@nestjs/common';
2
2
  import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
3
3
  import { DiscoveryService } from "@nestjs/core";
4
4
  import { EntityManager, Repository } from 'typeorm';
@@ -17,6 +17,8 @@ import { FieldMetadata } from '../entities/field-metadata.entity';
17
17
  import { ModelMetadata } from '../entities/model-metadata.entity';
18
18
  import { UpdateViewMetadataDto } from '../dtos/update-view-metadata.dto';
19
19
  import { ActionMetadataService } from './action-metadata.service';
20
+ import { SolidIntrospectService } from './solid-introspect.service';
21
+ import { BasicFilterDto } from 'src/dtos/basic-filters.dto';
20
22
 
21
23
  @Injectable()
22
24
  export class ViewMetadataService extends CRUDService<ViewMetadata> {
@@ -30,6 +32,7 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
30
32
  readonly discoveryService: DiscoveryService,
31
33
  readonly crudHelperService: CrudHelperService,
32
34
  readonly actionMetadataService: ActionMetadataService,
35
+ readonly introspectService: SolidIntrospectService,
33
36
  @InjectEntityManager()
34
37
  readonly entityManager: EntityManager,
35
38
  @InjectRepository(ViewMetadata, 'default')
@@ -58,21 +61,33 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
58
61
  relations: {
59
62
  model: {
60
63
  userKeyField: true, // Nested population of 'someOtherEntity' within 'model'
61
- },
64
+ },
62
65
  module: true,
63
66
  }
64
67
  });
65
- if (entity) {
66
- entity.layout = JSON.parse(entity.layout);
67
- if (entity?.layout?.attrs?.createAction) {
68
- const actionName: string = entity.layout.attrs.createAction;
69
- entity.layout.attrs.createAction = await this.actionMetadataService.findOneByUserKey(actionName)
70
- }
71
- if (entity?.layout?.attrs?.editAction) {
72
- const actionName: string = entity.layout.attrs.editAction;
73
- entity.layout.attrs.editAction = await this.actionMetadataService.findOneByUserKey(actionName)
74
- }
68
+
69
+ if (!entity) {
70
+ throw new BadRequestException(`Unable to identify view for module: ${moduleName}, model: ${modelName} and viewType: ${viewType}`);
71
+ }
72
+
73
+ // If view entity found then convert layout from "string" to "json".
74
+ entity.layout = JSON.parse(entity.layout);
75
+ if (entity?.layout?.attrs?.createAction) {
76
+ const actionName: string = entity.layout.attrs.createAction;
77
+ entity.layout.attrs.createAction = await this.actionMetadataService.findOneByUserKey(actionName)
78
+ }
79
+ if (entity?.layout?.attrs?.editAction) {
80
+ const actionName: string = entity.layout.attrs.editAction;
81
+ entity.layout.attrs.editAction = await this.actionMetadataService.findOneByUserKey(actionName)
75
82
  }
83
+
84
+ // for form views, we need to check if "workflow" field is configured, if configured then return an extra metadata "solidFormViewWorkflowData"
85
+ let workflowFieldName = null;
86
+ let workflowField = null;
87
+ if (viewType === 'form') {
88
+ workflowFieldName = entity.layout?.attrs?.workflowField;
89
+ }
90
+
76
91
  // We also need to fetch a map of fields.
77
92
  const fields = await this.fieldMetadataRepo.find({
78
93
  where: {
@@ -85,6 +100,11 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
85
100
  for (let i = 0; i < fields.length; i++) {
86
101
  const field = fields[i];
87
102
 
103
+ // We need to identify the workflowField metadata if specified.
104
+ if (workflowFieldName && field.name === workflowFieldName) {
105
+ workflowField = field;
106
+ }
107
+
88
108
  // For fields of type relation & relationType many-to-one
89
109
  // We fetch metadata regarding the relationCoModelSingularName
90
110
  if (field.type === 'relation') {
@@ -106,10 +126,38 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
106
126
  }
107
127
  }
108
128
 
109
- return {
129
+ // Check if we were able to resolve an actual workflowField.
130
+ let solidFormViewWorkflowData = [];
131
+ if (viewType === 'form' && workflowField) {
132
+ // check for type of workflow field.
133
+ // for workflowFields of type selectionStatic we simply return the key/values from field metadata AS-IS
134
+ if (workflowField.type === 'selectionStatic') {
135
+ solidFormViewWorkflowData = workflowField.selectionStaticValues.map(item => {
136
+ const [label, value] = item.split(":");
137
+ return { label, value };
138
+ });
139
+ }
140
+ // for workflowFields of type relation.many-to-one we need to query the co-model, and return data in key/value format.
141
+ if (workflowField.type === 'relation' && workflowField.relationType === 'many-to-one') {
142
+ const comodelCrudService = this.introspectService.getCRUDService(workflowField.relationCoModelSingularName);
143
+ const data = await comodelCrudService.find({ limit: 100, offset: 0, });
144
+ const records = data.records ?? [];
145
+ const workflowFieldMetadata = fieldsMap.get(workflowFieldName);
146
+ const workflowFielUserkey = workflowFieldMetadata['relationModel']['userKeyField']['name'];
147
+
148
+ // iterate over the comodel records extracting the label & value.
149
+ solidFormViewWorkflowData = records.map(item => ({ 'label': item[workflowFielUserkey], 'value': item['id'] }))
150
+ }
151
+
152
+ }
153
+
154
+ const r = {
110
155
  'solidView': entity,
111
156
  'solidFieldsMetadata': Object.fromEntries(fieldsMap),
112
- };
157
+ 'solidFormViewWorkflowData': solidFormViewWorkflowData
158
+ }
159
+
160
+ return r;
113
161
  }
114
162
 
115
163
  async findOneByUserKey(name: string, relations = {}) {
@@ -137,6 +185,7 @@ export class ViewMetadataService extends CRUDService<ViewMetadata> {
137
185
  return this.repo.save(viewData);
138
186
  }
139
187
  }
188
+
140
189
  async createIfNotPresent(updateSolidViewDto: UpdateViewMetadataDto) {
141
190
  // First check if module already exists using name
142
191
  const existingSolidView = await this.findOneByUserKey(updateSolidViewDto.name);