@solidxai/core 0.1.11-beta.7 → 0.1.11-beta.9

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.
Files changed (91) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/CLAUDE.md +26 -0
  3. package/dist/helpers/environment.helper.d.ts +1 -0
  4. package/dist/helpers/environment.helper.d.ts.map +1 -1
  5. package/dist/helpers/environment.helper.js +4 -0
  6. package/dist/helpers/environment.helper.js.map +1 -1
  7. package/dist/helpers/model-metadata-helper.service.d.ts +2 -1
  8. package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
  9. package/dist/helpers/model-metadata-helper.service.js +6 -2
  10. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  11. package/dist/helpers/typeorm-db-helper.d.ts +1 -0
  12. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  13. package/dist/helpers/typeorm-db-helper.js +12 -0
  14. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  15. package/dist/repository/scheduled-job.repository.d.ts +2 -2
  16. package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
  17. package/dist/repository/scheduled-job.repository.js +6 -3
  18. package/dist/repository/scheduled-job.repository.js.map +1 -1
  19. package/dist/repository/security-rule.repository.d.ts +2 -2
  20. package/dist/repository/security-rule.repository.d.ts.map +1 -1
  21. package/dist/repository/security-rule.repository.js +6 -3
  22. package/dist/repository/security-rule.repository.js.map +1 -1
  23. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
  24. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +2 -1
  25. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
  26. package/dist/services/crud-helper.service.d.ts.map +1 -1
  27. package/dist/services/crud-helper.service.js +25 -12
  28. package/dist/services/crud-helper.service.js.map +1 -1
  29. package/dist/services/model-metadata.service.d.ts.map +1 -1
  30. package/dist/services/model-metadata.service.js +2 -1
  31. package/dist/services/model-metadata.service.js.map +1 -1
  32. package/dist/services/module-metadata.service.d.ts.map +1 -1
  33. package/dist/services/module-metadata.service.js +2 -1
  34. package/dist/services/module-metadata.service.js.map +1 -1
  35. package/dist/services/settings/default-settings-provider.service.d.ts +32 -0
  36. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  37. package/dist/services/settings/default-settings-provider.service.js +14 -0
  38. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  39. package/dist/subscribers/audit.subscriber.d.ts +0 -1
  40. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  41. package/dist/subscribers/audit.subscriber.js +5 -8
  42. package/dist/subscribers/audit.subscriber.js.map +1 -1
  43. package/dist/subscribers/computed-entity-field.subscriber.d.ts +9 -1
  44. package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
  45. package/dist/subscribers/computed-entity-field.subscriber.js +43 -12
  46. package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
  47. package/dist/subscribers/scheduled-job.subscriber.d.ts.map +1 -1
  48. package/dist/subscribers/scheduled-job.subscriber.js +3 -2
  49. package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
  50. package/dist/subscribers/security-rule.subscriber.d.ts.map +1 -1
  51. package/dist/subscribers/security-rule.subscriber.js +5 -3
  52. package/dist/subscribers/security-rule.subscriber.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/helpers/environment.helper.ts +11 -0
  55. package/src/helpers/model-metadata-helper.service.ts +7 -2
  56. package/src/helpers/typeorm-db-helper.ts +15 -0
  57. package/src/repository/scheduled-job.repository.ts +10 -4
  58. package/src/repository/security-rule.repository.ts +10 -4
  59. package/src/services/1.js +6 -0
  60. package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +5 -4
  61. package/src/services/crud-helper.service.ts +25 -12
  62. package/src/services/model-metadata.service.ts +2 -1
  63. package/src/services/module-metadata.service.ts +2 -1
  64. package/src/services/settings/default-settings-provider.service.ts +14 -0
  65. package/src/subscribers/audit.subscriber.ts +7 -9
  66. package/src/subscribers/computed-entity-field.subscriber.ts +79 -28
  67. package/src/subscribers/scheduled-job.subscriber.ts +3 -2
  68. package/src/subscribers/security-rule.subscriber.ts +8 -3
  69. package/dist-tests/api/authenticate.spec.js +0 -119
  70. package/dist-tests/api/authenticate.spec.js.map +0 -1
  71. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
  72. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
  73. package/dist-tests/api/ping.spec.js +0 -21
  74. package/dist-tests/api/ping.spec.js.map +0 -1
  75. package/dist-tests/helpers/auth.js +0 -41
  76. package/dist-tests/helpers/auth.js.map +0 -1
  77. package/dist-tests/helpers/env.js +0 -11
  78. package/dist-tests/helpers/env.js.map +0 -1
  79. package/docs/agent-hub-grooming.md +0 -301
  80. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
  81. package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
  82. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
  83. package/docs/datasource-introspection-ddl-analysis.md +0 -326
  84. package/docs/datasource-introspection-implementation-plan.md +0 -306
  85. package/docs/grouping-enhancements.md +0 -89
  86. package/docs/java-spring/README.md +0 -3
  87. package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
  88. package/docs/module-package-import-handoff.md +0 -691
  89. package/docs/seed-changes.md +0 -65
  90. package/docs/test-data-workflow.md +0 -200
  91. package/docs/type-declaration-import-issue.md +0 -24
@@ -5,6 +5,7 @@ import { ActiveUserData } from "src/interfaces/active-user-data.interface";
5
5
  import { SolidRegistry } from "src/helpers/solid-registry";
6
6
  import { BadRequestException, Logger } from "@nestjs/common";
7
7
  import { ERROR_MESSAGES } from "src/constants/error-messages";
8
+ import { buildCastToText } from "src/helpers/typeorm-db-helper";
8
9
 
9
10
  export enum FilterCombinator {
10
11
  AND = '$and',
@@ -101,15 +102,19 @@ export class CrudHelperService {
101
102
  case '$eq':
102
103
  qb.andWhere(`${colExpr} = :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$eq });
103
104
  break;
104
- case '$eqi':
105
- qb.andWhere(`LOWER(${colExpr}) = :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$eqi.toLowerCase() });
105
+ case '$eqi': {
106
+ const castExpr = buildCastToText(this.getDriver(qb), colExpr);
107
+ qb.andWhere(`LOWER(${castExpr}) = :${uniqueFieldAlias}`, { [uniqueFieldAlias]: String(normalizedPrimaryOperatorObj.$eqi).toLowerCase() });
106
108
  break;
109
+ }
107
110
  case '$ne':
108
111
  qb.andWhere(`${colExpr} != :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$ne });
109
112
  break;
110
- case '$nei':
111
- qb.andWhere(`LOWER(${colExpr}) != :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$nei.toLowerCase() });
113
+ case '$nei': {
114
+ const castExpr = buildCastToText(this.getDriver(qb), colExpr);
115
+ qb.andWhere(`LOWER(${castExpr}) != :${uniqueFieldAlias}`, { [uniqueFieldAlias]: String(normalizedPrimaryOperatorObj.$nei).toLowerCase() });
112
116
  break;
117
+ }
113
118
  case '$gt':
114
119
  qb.andWhere(`${colExpr} > :${uniqueFieldAlias}`, { [uniqueFieldAlias]: normalizedPrimaryOperatorObj.$gt });
115
120
  break;
@@ -134,12 +139,16 @@ export class CrudHelperService {
134
139
  case '$notContains':
135
140
  qb.andWhere(`${colExpr} NOT LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${normalizedPrimaryOperatorObj.$notContains}%` });
136
141
  break;
137
- case '$containsi':
138
- qb.andWhere(`LOWER(${colExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${normalizedPrimaryOperatorObj.$containsi.toLowerCase()}%` });
142
+ case '$containsi': {
143
+ const castExpr = buildCastToText(this.getDriver(qb), colExpr);
144
+ qb.andWhere(`LOWER(${castExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${String(normalizedPrimaryOperatorObj.$containsi).toLowerCase()}%` });
139
145
  break;
140
- case '$notContainsi':
141
- qb.andWhere(`LOWER(${colExpr}) NOT LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${normalizedPrimaryOperatorObj.$notContainsi.toLowerCase()}%` });
146
+ }
147
+ case '$notContainsi': {
148
+ const castExpr = buildCastToText(this.getDriver(qb), colExpr);
149
+ qb.andWhere(`LOWER(${castExpr}) NOT LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${String(normalizedPrimaryOperatorObj.$notContainsi).toLowerCase()}%` });
142
150
  break;
151
+ }
143
152
  case '$null':
144
153
  qb.andWhere(`${colExpr} IS NULL`);
145
154
  break;
@@ -152,15 +161,19 @@ export class CrudHelperService {
152
161
  case '$startsWith':
153
162
  qb.andWhere(`${colExpr} LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `${normalizedPrimaryOperatorObj.$startsWith}%` });
154
163
  break;
155
- case '$startsWithi':
156
- qb.andWhere(`LOWER(${colExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `${normalizedPrimaryOperatorObj.$startsWithi.toLowerCase()}%` });
164
+ case '$startsWithi': {
165
+ const castExpr = buildCastToText(this.getDriver(qb), colExpr);
166
+ qb.andWhere(`LOWER(${castExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `${String(normalizedPrimaryOperatorObj.$startsWithi).toLowerCase()}%` });
157
167
  break;
168
+ }
158
169
  case '$endsWith':
159
170
  qb.andWhere(`${colExpr} LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${normalizedPrimaryOperatorObj.$endsWith}` });
160
171
  break;
161
- case '$endsWithi':
162
- qb.andWhere(`LOWER(${colExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${normalizedPrimaryOperatorObj.$endsWithi.toLowerCase()}` });
172
+ case '$endsWithi': {
173
+ const castExpr = buildCastToText(this.getDriver(qb), colExpr);
174
+ qb.andWhere(`LOWER(${castExpr}) LIKE :${uniqueFieldAlias}`, { [uniqueFieldAlias]: `%${String(normalizedPrimaryOperatorObj.$endsWithi).toLowerCase()}` });
163
175
  break;
176
+ }
164
177
  default:
165
178
  throw new Error(`Operator ${operator} is not supported`);
166
179
  }
@@ -27,6 +27,7 @@ import { PermissionMetadata } from '../entities/permission-metadata.entity';
27
27
  import { RoleMetadata } from '../entities/role-metadata.entity';
28
28
  import { ViewMetadata } from '../entities/view-metadata.entity';
29
29
  import { CommandService } from '../helpers/command.service';
30
+ import { isEmbeddedDb } from '../helpers/environment.helper';
30
31
  import { startNodemonHeartbeat } from '../helpers/nodemon-heartbeat';
31
32
  import {
32
33
  REFRESH_MODEL_COMMAND,
@@ -968,7 +969,7 @@ export class ModelMetadataService {
968
969
  @DisallowInProduction()
969
970
  async generateCodeViaCtl(modelId: number): Promise<string> {
970
971
  const model = await this.findOne(modelId);
971
- const isEmbedded = process.env.DEFAULT_DATABASE_DRIVER === 'pglite';
972
+ const isEmbedded = isEmbeddedDb();
972
973
 
973
974
  // When using an embedded PGlite database, the single-connection limit means
974
975
  // a spawned `solid refresh-model` subprocess cannot get its own DB connection
@@ -26,6 +26,7 @@ import {
26
26
  } from '../helpers/schematic.service';
27
27
  import { CommandService } from '../helpers/command.service';
28
28
  import { SolidRegistry } from '../helpers/solid-registry';
29
+ import { isEmbeddedDb } from '../helpers/environment.helper';
29
30
  import { startNodemonHeartbeat } from '../helpers/nodemon-heartbeat';
30
31
  import { CodeGenerationOptions, ModuleMetadataConfiguration } from '../interfaces';
31
32
  import { CrudHelperService } from './crud-helper.service';
@@ -535,7 +536,7 @@ export class ModuleMetadataService {
535
536
  @DisallowInProduction()
536
537
  async generateCodeViaCtl(moduleId: number): Promise<string> {
537
538
  const module = await this.findOne(moduleId);
538
- const isEmbedded = process.env.DEFAULT_DATABASE_DRIVER === 'pglite';
539
+ const isEmbedded = isEmbeddedDb();
539
540
 
540
541
  // When using an embedded PGlite database, the single-connection limit means
541
542
  // a spawned `solid refresh-module` subprocess cannot get its own DB connection
@@ -114,6 +114,20 @@ const getSolidCoreSettings = (isProd: boolean) =>
114
114
  { label: "In Image View", value: "in_image_view" },
115
115
  ],
116
116
  },
117
+ {
118
+ moduleName: "solid-core",
119
+ key: "rowClickAction",
120
+ value: "edit",
121
+ level: SettingLevel.SystemAdminEditable,
122
+ label: "Row Click Action",
123
+ group: "app-settings",
124
+ sortOrder: 45,
125
+ controlType: "selectionStatic",
126
+ options: [
127
+ { label: "Edit", value: "edit" },
128
+ { label: "View", value: "view" },
129
+ ],
130
+ },
117
131
  {
118
132
  moduleName: "solid-core",
119
133
  key: "showAuthContent",
@@ -4,6 +4,7 @@ import { lowerFirst } from 'src/helpers/string.helper';
4
4
  import { SolidRegistry } from 'src/helpers/solid-registry';
5
5
  import { DataSource, EntityMetadata, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
6
6
  import { AuditQueuePayload } from 'src/interfaces';
7
+ import { isEmbeddedDb } from 'src/helpers/environment.helper';
7
8
  import { RequestContextService } from 'src/services/request-context.service';
8
9
  import { PublisherFactory } from 'src/services/queues/publisher-factory.service';
9
10
  const AUDIT_BEFORE_SNAPSHOT = '__auditBeforeSnapshot';
@@ -88,7 +89,10 @@ export class AuditSubscriber implements EntitySubscriberInterface {
88
89
  */
89
90
  private async prepareManyToManyAuditUpdateSnapshot(event: UpdateEvent<any>, entityId: number | null, updatedColumnNames: string[]): Promise<{ before: any; after: any } | null> {
90
91
 
91
- const auditRelationFields = (await this.modelMetadataHelperService.loadFieldHierarchy(lowerFirst(event.metadata.name))).filter(field =>
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 =>
92
96
  field.enableAuditTracking &&
93
97
  field.type === 'relation' &&
94
98
  field.relationType !== 'one-to-many'
@@ -106,7 +110,7 @@ export class AuditSubscriber implements EntitySubscriberInterface {
106
110
 
107
111
  const relationBefore = event.entity?.[AUDIT_BEFORE_SNAPSHOT] ?? null;
108
112
 
109
- const relationAfter = await event.manager.getRepository(event.metadata.target as any).findOne({
113
+ const relationAfter = await mgr.getRepository(event.metadata.target as any).findOne({
110
114
  where: { id: entityId } as any,
111
115
  relations: relations as any,
112
116
  });
@@ -151,7 +155,7 @@ export class AuditSubscriber implements EntitySubscriberInterface {
151
155
 
152
156
  if (batch.length === 0) return;
153
157
 
154
- if (this.shouldDeferAuditPublish()) {
158
+ if (isEmbeddedDb()) {
155
159
  setImmediate(() => void this.publishAuditBatch(batch));
156
160
  return;
157
161
  }
@@ -164,12 +168,6 @@ export class AuditSubscriber implements EntitySubscriberInterface {
164
168
  this.perTxn.delete(event.queryRunner);
165
169
  }
166
170
 
167
- private shouldDeferAuditPublish(): boolean {
168
- const driver = (process.env.DEFAULT_DATABASE_DRIVER ?? '').toLowerCase();
169
- const poolMax = Number(process.env.DEFAULT_DATABASE_POOL_MAX ?? 20);
170
- return driver === 'pglite' && Number.isFinite(poolMax) && poolMax <= 1;
171
- }
172
-
173
171
  private async publishAuditBatch(batch: AuditQueuePayload[]) {
174
172
  try {
175
173
  // allSettled: publish in parallel; a single failure does not block the rest.
@@ -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.handleComputedFieldEvaluationJob(event.entity, ComputedFieldTriggerOperation.afterInsert, modelName, eventContext);
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
- // this.handleComputedFieldEvaluationJob(event.databaseEntity, ComputedFieldTriggerOperation.afterUpdate, modelName, eventContext);
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.handleComputedFieldEvaluationJob(event.databaseEntity, ComputedFieldTriggerOperation.afterRemove, modelName, eventContext);
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
- private async handleComputedFieldEvaluation(entity: any, currentOperation: ComputedFieldTriggerOperation, modelName: string, eventContext?: TypeOrmEventContext): Promise<void> {
72
- if (!entity) {
73
- return;
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
- //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
81
- for (const computedField of computedFieldsTobeEvaluated) {
82
- await this.evaluateComputedField(this.attachContext(computedField, eventContext), entity, currentOperation);
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
- private handleComputedFieldEvaluationJob(entity: any, currentOperation: ComputedFieldTriggerOperation, modelName: string, eventContext?: TypeOrmEventContext) {
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.enqueueComputedFieldEvaluationJob(this.attachContext(computedField, eventContext), entity, eventContext);
148
+ await this.evaluateComputedField(this.attachContext(computedField, eventContext), entity, currentOperation);
98
149
  }
99
150
  }
100
151
 
@@ -147,17 +198,14 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface
147
198
  }
148
199
  }
149
200
 
150
- private enqueueComputedFieldEvaluationJob(computedField: ComputedFieldMetadata<any>, databaseEntity: any, eventContext?: any) {
201
+ private enqueueComputedFieldEvaluationJob(computedField: ComputedFieldMetadata<any>, databaseEntity: any) {
151
202
  const { manager: _manager, ...serializableEventContext } = computedField.eventContext ?? {};
152
203
  const payload = {
153
204
  ...computedField,
154
205
  eventContext: serializableEventContext,
155
206
  databaseEntity,
156
207
  };
157
- this.publisherFactory.publish({ payload }, 'ComputedFieldEvaluationPublisher')
158
- // this.computedFieldPublisher.publish({
159
- // payload
160
- // });
208
+ this.publisherFactory.publish({ payload }, 'ComputedFieldEvaluationPublisher');
161
209
  }
162
210
 
163
211
  private attachContext<T extends ComputedFieldMetadata<any>>(computedField: T, eventContext?: any): T {
@@ -176,7 +224,10 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface
176
224
  const base: TypeOrmEventContext = {
177
225
  metadataName: event.metadata?.name,
178
226
  eventType: eventType,
179
- manager: event.manager,
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 } : {}),
180
231
  };
181
232
  if ("entityId" in event && event.entityId) {
182
233
  base.entityId = event.entityId;
@@ -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,
@@ -4,6 +4,7 @@ import * as fs from 'fs/promises'; // Use the Promise-based version of fs for as
4
4
  import { SOLID_CORE_MODULE_NAME } from 'src/constants';
5
5
  import { ModelMetadata } from 'src/entities/model-metadata.entity';
6
6
  import { SecurityRule } from 'src/entities/security-rule.entity';
7
+ import { isEmbeddedDb } from "src/helpers/environment.helper";
7
8
  import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service";
8
9
  import { SecurityRuleRepository } from 'src/repository/security-rule.repository';
9
10
  import { DataSource, EntitySubscriberInterface, InsertEvent, UpdateEvent } from "typeorm";
@@ -40,7 +41,11 @@ export class SecurityRuleSubscriber implements EntitySubscriberInterface<Securit
40
41
  return;
41
42
  }
42
43
 
43
- const modelMetadataRepo = event.manager.getRepository(ModelMetadata);
44
+ // On embedded PGlite, use the transaction's connection to avoid a
45
+ // second pooled connection mid-transaction. On regular Postgres, use
46
+ // the default DataSource (original behaviour).
47
+ const mgr = isEmbeddedDb() ? event.manager : this.dataSource;
48
+ const modelMetadataRepo = mgr.getRepository(ModelMetadata);
44
49
  const populatedModelMetadata = await modelMetadataRepo.findOne({
45
50
  where: {
46
51
  id: modelMetadata.id
@@ -67,12 +72,12 @@ export class SecurityRuleSubscriber implements EntitySubscriberInterface<Securit
67
72
 
68
73
  if (metaData.securityRules) {
69
74
  const securityRuleIndex = metaData.securityRules?.findIndex((ruleFromFile: { name: string }) => ruleFromFile.name === securityRule.name);
70
- const { id, roleId, modelMetadataId, ...requiredDto } = await this.securityRuleRepo.toDto(securityRule)
75
+ const { id, roleId, modelMetadataId, ...requiredDto } = await this.securityRuleRepo.toDto(securityRule, isEmbeddedDb() ? event.manager : undefined)
71
76
  metaData.securityRules[securityRuleIndex] = { ...requiredDto, securityRuleConfig: JSON.parse(securityRule.securityRuleConfig) }
72
77
  }
73
78
  else {
74
79
  const securityRules = []
75
- const { id, roleId, modelMetadataId, ...requiredDto } = await this.securityRuleRepo.toDto(securityRule)
80
+ const { id, roleId, modelMetadataId, ...requiredDto } = await this.securityRuleRepo.toDto(securityRule, isEmbeddedDb() ? event.manager : undefined)
76
81
  securityRules.push({ ...requiredDto, securityRuleConfig: JSON.parse(securityRule.securityRuleConfig) })
77
82
  metaData.securityRules = securityRules
78
83
  }
@@ -1,119 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const test_1 = require("@playwright/test");
4
- const env_1 = require("../helpers/env");
5
- const baseURL = process.env.API_BASE_URL ?? "http://localhost:3000";
6
- const TEST_USER_EMAIL = (0, env_1.getRequiredEnv)("TEST_USER_EMAIL");
7
- const TEST_USER_PASSWORD = (0, env_1.getRequiredEnv)("TEST_USER_PASSWORD");
8
- function base64UrlDecode(input) {
9
- const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
10
- const padded = normalized.length % 4 === 0
11
- ? normalized
12
- : normalized.padEnd(normalized.length + (4 - (normalized.length % 4)), "=");
13
- return Buffer.from(padded, "base64").toString("utf-8");
14
- }
15
- function validateJwt(token) {
16
- const parts = token.split(".");
17
- if (parts.length !== 3) {
18
- throw new Error("JWT must have three dot-separated parts.");
19
- }
20
- const headerJson = JSON.parse(base64UrlDecode(parts[0]));
21
- const payloadJson = JSON.parse(base64UrlDecode(parts[1]));
22
- if (!headerJson || typeof headerJson !== "object") {
23
- throw new Error("JWT header must be a JSON object.");
24
- }
25
- if (!payloadJson || typeof payloadJson !== "object") {
26
- throw new Error("JWT payload must be a JSON object.");
27
- }
28
- if (typeof payloadJson.exp !== "number") {
29
- throw new Error("JWT payload.exp must be a number.");
30
- }
31
- return payloadJson;
32
- }
33
- (0, test_1.test)("API: authenticate succeeds with valid credentials", async () => {
34
- const api = await test_1.request.newContext({
35
- baseURL,
36
- extraHTTPHeaders: {
37
- accept: "*/*",
38
- "content-type": "application/json",
39
- },
40
- });
41
- try {
42
- const res = await api.post("/api/iam/authenticate", {
43
- data: {
44
- email: TEST_USER_EMAIL,
45
- username: "",
46
- password: TEST_USER_PASSWORD,
47
- },
48
- });
49
- (0, test_1.expect)(res.status()).toBe(200);
50
- const json = await res.json();
51
- (0, test_1.expect)(json.statusCode).toBe(200);
52
- (0, test_1.expect)(Array.isArray(json.message)).toBe(true);
53
- (0, test_1.expect)(json.message.length).toBe(0);
54
- (0, test_1.expect)(json.error).toBe("");
55
- const user = json.data?.user;
56
- (0, test_1.expect)(user, "Expected data.user to be an object.").toBeTruthy();
57
- (0, test_1.expect)(typeof user).toBe("object");
58
- const email = user?.email;
59
- (0, test_1.expect)(typeof email).toBe("string");
60
- if (email === TEST_USER_EMAIL) {
61
- (0, test_1.expect)(email).toBe(TEST_USER_EMAIL);
62
- }
63
- else {
64
- (0, test_1.expect)(email.length).toBeGreaterThan(0);
65
- }
66
- (0, test_1.expect)(typeof user?.mobile).toBe("string");
67
- (0, test_1.expect)(typeof user?.username).toBe("string");
68
- (0, test_1.expect)(typeof user?.forcePasswordChange).toBe("boolean");
69
- (0, test_1.expect)(typeof user?.id).toBe("number");
70
- const roles = user?.roles;
71
- (0, test_1.expect)(Array.isArray(roles)).toBe(true);
72
- if (Array.isArray(roles)) {
73
- (0, test_1.expect)(roles.every((role) => typeof role === "string")).toBe(true);
74
- (0, test_1.expect)(roles).toContain("Admin");
75
- }
76
- const accessToken = json.data?.accessToken;
77
- const refreshToken = json.data?.refreshToken;
78
- (0, test_1.expect)(typeof accessToken).toBe("string");
79
- (0, test_1.expect)(typeof refreshToken).toBe("string");
80
- const accessPayload = validateJwt(accessToken);
81
- const refreshPayload = validateJwt(refreshToken);
82
- (0, test_1.expect)(typeof accessPayload.exp).toBe("number");
83
- (0, test_1.expect)(typeof refreshPayload.exp).toBe("number");
84
- }
85
- finally {
86
- await api.dispose();
87
- }
88
- });
89
- (0, test_1.test)("API: authenticate fails with wrong password", async () => {
90
- const api = await test_1.request.newContext({
91
- baseURL,
92
- extraHTTPHeaders: {
93
- accept: "*/*",
94
- "content-type": "application/json",
95
- },
96
- });
97
- try {
98
- const res = await api.post("/api/iam/authenticate", {
99
- data: {
100
- email: TEST_USER_EMAIL,
101
- username: "",
102
- password: `${TEST_USER_PASSWORD}__wrong`,
103
- },
104
- });
105
- (0, test_1.expect)(res.status()).toBe(401);
106
- const json = await res.json();
107
- (0, test_1.expect)(json.statusCode).toBe(401);
108
- (0, test_1.expect)(json.statusCodeMessage).toBe("Unauthorized");
109
- (0, test_1.expect)(json.message).toBe("Invalid credentials");
110
- (0, test_1.expect)(json.error).toBe("Invalid credentials");
111
- (0, test_1.expect)(json.data?.statusCode).toBe(401);
112
- (0, test_1.expect)(json.data?.error).toBe("Unauthorized");
113
- (0, test_1.expect)(json.data?.message).toBe("Invalid credentials");
114
- }
115
- finally {
116
- await api.dispose();
117
- }
118
- });
119
- //# sourceMappingURL=authenticate.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"authenticate.spec.js","sourceRoot":"","sources":["../../tests/api/authenticate.spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, request, test } from \"@playwright/test\";\nimport { getRequiredEnv } from \"../helpers/env\";\n\ntype JwtPayload = {\n exp?: number;\n [key: string]: unknown;\n};\n\nconst baseURL = process.env.API_BASE_URL ?? \"http://localhost:3000\";\nconst TEST_USER_EMAIL = getRequiredEnv(\"TEST_USER_EMAIL\");\nconst TEST_USER_PASSWORD = getRequiredEnv(\"TEST_USER_PASSWORD\");\n\nfunction base64UrlDecode(input: string): string {\n const normalized = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded =\n normalized.length % 4 === 0\n ? normalized\n : normalized.padEnd(\n normalized.length + (4 - (normalized.length % 4)),\n \"=\"\n );\n return Buffer.from(padded, \"base64\").toString(\"utf-8\");\n}\n\nfunction validateJwt(token: string): JwtPayload {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n throw new Error(\"JWT must have three dot-separated parts.\");\n }\n\n const headerJson = JSON.parse(base64UrlDecode(parts[0]));\n const payloadJson = JSON.parse(base64UrlDecode(parts[1]));\n\n if (!headerJson || typeof headerJson !== \"object\") {\n throw new Error(\"JWT header must be a JSON object.\");\n }\n\n if (!payloadJson || typeof payloadJson !== \"object\") {\n throw new Error(\"JWT payload must be a JSON object.\");\n }\n\n if (typeof payloadJson.exp !== \"number\") {\n throw new Error(\"JWT payload.exp must be a number.\");\n }\n\n return payloadJson as JwtPayload;\n}\n\ntest(\"API: authenticate succeeds with valid credentials\", async () => {\n const api = await request.newContext({\n baseURL,\n extraHTTPHeaders: {\n accept: \"*/*\",\n \"content-type\": \"application/json\",\n },\n });\n\n try {\n const res = await api.post(\"/api/iam/authenticate\", {\n data: {\n email: TEST_USER_EMAIL,\n username: \"\",\n password: TEST_USER_PASSWORD,\n },\n });\n\n expect(res.status()).toBe(200);\n const json = await res.json();\n\n expect(json.statusCode).toBe(200);\n expect(Array.isArray(json.message)).toBe(true);\n expect(json.message.length).toBe(0);\n expect(json.error).toBe(\"\");\n\n const user = json.data?.user as Record<string, unknown> | undefined;\n expect(user, \"Expected data.user to be an object.\").toBeTruthy();\n expect(typeof user).toBe(\"object\");\n\n const email = user?.email;\n expect(typeof email).toBe(\"string\");\n if (email === TEST_USER_EMAIL) {\n expect(email).toBe(TEST_USER_EMAIL);\n } else {\n // If your API returns a fixed system email, replace this with an exact match.\n expect((email as string).length).toBeGreaterThan(0);\n }\n\n expect(typeof user?.mobile).toBe(\"string\");\n expect(typeof user?.username).toBe(\"string\");\n expect(typeof user?.forcePasswordChange).toBe(\"boolean\");\n expect(typeof user?.id).toBe(\"number\");\n\n const roles = user?.roles;\n expect(Array.isArray(roles)).toBe(true);\n if (Array.isArray(roles)) {\n expect(roles.every((role) => typeof role === \"string\")).toBe(true);\n expect(roles).toContain(\"Admin\");\n }\n\n const accessToken = json.data?.accessToken;\n const refreshToken = json.data?.refreshToken;\n expect(typeof accessToken).toBe(\"string\");\n expect(typeof refreshToken).toBe(\"string\");\n\n const accessPayload = validateJwt(accessToken as string);\n const refreshPayload = validateJwt(refreshToken as string);\n\n expect(typeof accessPayload.exp).toBe(\"number\");\n expect(typeof refreshPayload.exp).toBe(\"number\");\n } finally {\n await api.dispose();\n }\n});\n\ntest(\"API: authenticate fails with wrong password\", async () => {\n const api = await request.newContext({\n baseURL,\n extraHTTPHeaders: {\n accept: \"*/*\",\n \"content-type\": \"application/json\",\n },\n });\n\n try {\n const res = await api.post(\"/api/iam/authenticate\", {\n data: {\n email: TEST_USER_EMAIL,\n username: \"\",\n password: `${TEST_USER_PASSWORD}__wrong`,\n },\n });\n\n expect(res.status()).toBe(401);\n const json = await res.json();\n\n expect(json.statusCode).toBe(401);\n expect(json.statusCodeMessage).toBe(\"Unauthorized\");\n expect(json.message).toBe(\"Invalid credentials\");\n expect(json.error).toBe(\"Invalid credentials\");\n expect(json.data?.statusCode).toBe(401);\n expect(json.data?.error).toBe(\"Unauthorized\");\n expect(json.data?.message).toBe(\"Invalid credentials\");\n } finally {\n await api.dispose();\n }\n});\n"]}