@solidxai/core 0.1.11-beta.0 → 0.1.11-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/CLAUDE.md +26 -0
  3. package/dist/commands/migrate-removed-fields.command.d.ts +19 -0
  4. package/dist/commands/migrate-removed-fields.command.d.ts.map +1 -0
  5. package/dist/commands/migrate-removed-fields.command.js +77 -0
  6. package/dist/commands/migrate-removed-fields.command.js.map +1 -0
  7. package/dist/controllers/datasource-introspection.controller.d.ts +46 -1
  8. package/dist/controllers/datasource-introspection.controller.d.ts.map +1 -1
  9. package/dist/controllers/datasource-introspection.controller.js +38 -0
  10. package/dist/controllers/datasource-introspection.controller.js.map +1 -1
  11. package/dist/controllers/service.controller.d.ts +1 -0
  12. package/dist/controllers/service.controller.d.ts.map +1 -1
  13. package/dist/controllers/service.controller.js +2 -2
  14. package/dist/controllers/service.controller.js.map +1 -1
  15. package/dist/dtos/datasource-introspection-mapping.dto.d.ts +5 -0
  16. package/dist/dtos/datasource-introspection-mapping.dto.d.ts.map +1 -1
  17. package/dist/dtos/datasource-introspection-mapping.dto.js +24 -2
  18. package/dist/dtos/datasource-introspection-mapping.dto.js.map +1 -1
  19. package/dist/entities/{legacy-common-with-id.entity.d.ts → legacy-common-with-generated-id.entity.d.ts} +2 -2
  20. package/dist/entities/legacy-common-with-generated-id.entity.d.ts.map +1 -0
  21. package/dist/entities/{legacy-common-with-id.entity.js → legacy-common-with-generated-id.entity.js} +4 -4
  22. package/dist/entities/legacy-common-with-generated-id.entity.js.map +1 -0
  23. package/dist/entities/{legacy-common.entity.d.ts → legacy-common.entity-with-existing-id.entity.d.ts} +1 -1
  24. package/dist/entities/legacy-common.entity-with-existing-id.entity.d.ts.map +1 -0
  25. package/dist/entities/{legacy-common.entity.js → legacy-common.entity-with-existing-id.entity.js} +1 -1
  26. package/dist/entities/legacy-common.entity-with-existing-id.entity.js.map +1 -0
  27. package/dist/helpers/command.service.d.ts +2 -0
  28. package/dist/helpers/command.service.d.ts.map +1 -1
  29. package/dist/helpers/command.service.js +6 -2
  30. package/dist/helpers/command.service.js.map +1 -1
  31. package/dist/helpers/environment.helper.d.ts +1 -0
  32. package/dist/helpers/environment.helper.d.ts.map +1 -1
  33. package/dist/helpers/environment.helper.js +4 -0
  34. package/dist/helpers/environment.helper.js.map +1 -1
  35. package/dist/helpers/field-crud-managers/IntFieldCrudManager.d.ts.map +1 -1
  36. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js +4 -1
  37. package/dist/helpers/field-crud-managers/IntFieldCrudManager.js.map +1 -1
  38. package/dist/helpers/model-metadata-helper.service.d.ts +2 -1
  39. package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
  40. package/dist/helpers/model-metadata-helper.service.js +9 -5
  41. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  42. package/dist/helpers/nodemon-heartbeat.d.ts +5 -0
  43. package/dist/helpers/nodemon-heartbeat.d.ts.map +1 -0
  44. package/dist/helpers/nodemon-heartbeat.js +67 -0
  45. package/dist/helpers/nodemon-heartbeat.js.map +1 -0
  46. package/dist/helpers/solid-registry.d.ts +2 -1
  47. package/dist/helpers/solid-registry.d.ts.map +1 -1
  48. package/dist/helpers/solid-registry.js.map +1 -1
  49. package/dist/helpers/typeorm-db-helper.d.ts +1 -0
  50. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  51. package/dist/helpers/typeorm-db-helper.js +12 -0
  52. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  53. package/dist/index.d.ts +3 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/interfaces/datasource-introspection-provider.interface.d.ts +1 -0
  58. package/dist/interfaces/datasource-introspection-provider.interface.d.ts.map +1 -1
  59. package/dist/interfaces/datasource-introspection-provider.interface.js.map +1 -1
  60. package/dist/interfaces.d.ts +1 -0
  61. package/dist/interfaces.d.ts.map +1 -1
  62. package/dist/interfaces.js.map +1 -1
  63. package/dist/repository/scheduled-job.repository.d.ts +5 -2
  64. package/dist/repository/scheduled-job.repository.d.ts.map +1 -1
  65. package/dist/repository/scheduled-job.repository.js +66 -3
  66. package/dist/repository/scheduled-job.repository.js.map +1 -1
  67. package/dist/repository/security-rule.repository.d.ts +2 -2
  68. package/dist/repository/security-rule.repository.d.ts.map +1 -1
  69. package/dist/repository/security-rule.repository.js +6 -3
  70. package/dist/repository/security-rule.repository.js.map +1 -1
  71. package/dist/seeders/module-metadata-seeder.service.d.ts +43 -2
  72. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  73. package/dist/seeders/module-metadata-seeder.service.js +1173 -232
  74. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  75. package/dist/seeders/seed-data/solid-core-metadata.json +128 -34
  76. package/dist/services/action-metadata.service.d.ts +7 -0
  77. package/dist/services/action-metadata.service.d.ts.map +1 -1
  78. package/dist/services/action-metadata.service.js +76 -3
  79. package/dist/services/action-metadata.service.js.map +1 -1
  80. package/dist/services/api-key.service.d.ts.map +1 -1
  81. package/dist/services/api-key.service.js.map +1 -1
  82. package/dist/services/authentication.service.d.ts +2 -0
  83. package/dist/services/authentication.service.d.ts.map +1 -1
  84. package/dist/services/authentication.service.js +32 -11
  85. package/dist/services/authentication.service.js.map +1 -1
  86. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.d.ts.map +1 -1
  87. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js +7 -5
  88. package/dist/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.js.map +1 -1
  89. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts +4 -0
  90. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.d.ts.map +1 -1
  91. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js +18 -1
  92. package/dist/services/computed-fields/entity/concat-entity-computed-field-provider.service.js.map +1 -1
  93. package/dist/services/crud-helper.service.d.ts.map +1 -1
  94. package/dist/services/crud-helper.service.js +25 -12
  95. package/dist/services/crud-helper.service.js.map +1 -1
  96. package/dist/services/crud.service.d.ts +1 -0
  97. package/dist/services/crud.service.d.ts.map +1 -1
  98. package/dist/services/crud.service.js +27 -0
  99. package/dist/services/crud.service.js.map +1 -1
  100. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.d.ts.map +1 -1
  101. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js +12 -0
  102. package/dist/services/datasource-introspection/mssql-datasource-introspection-provider.service.js.map +1 -1
  103. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.d.ts.map +1 -1
  104. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js +2 -0
  105. package/dist/services/datasource-introspection/mysql-datasource-introspection-provider.service.js.map +1 -1
  106. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.d.ts.map +1 -1
  107. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js +28 -15
  108. package/dist/services/datasource-introspection/postgres-datasource-introspection-provider.service.js.map +1 -1
  109. package/dist/services/datasource-introspection.service.d.ts +62 -2
  110. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  111. package/dist/services/datasource-introspection.service.js +654 -186
  112. package/dist/services/datasource-introspection.service.js.map +1 -1
  113. package/dist/services/import-transaction.service.d.ts.map +1 -1
  114. package/dist/services/import-transaction.service.js +3 -0
  115. package/dist/services/import-transaction.service.js.map +1 -1
  116. package/dist/services/locale.service.d.ts +14 -0
  117. package/dist/services/locale.service.d.ts.map +1 -1
  118. package/dist/services/locale.service.js +83 -0
  119. package/dist/services/locale.service.js.map +1 -1
  120. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +10 -10
  121. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  122. package/dist/services/menu-item-metadata.service.d.ts.map +1 -1
  123. package/dist/services/menu-item-metadata.service.js +1 -0
  124. package/dist/services/menu-item-metadata.service.js.map +1 -1
  125. package/dist/services/model-metadata.service.d.ts +3 -0
  126. package/dist/services/model-metadata.service.d.ts.map +1 -1
  127. package/dist/services/model-metadata.service.js +78 -0
  128. package/dist/services/model-metadata.service.js.map +1 -1
  129. package/dist/services/module-metadata.service.d.ts.map +1 -1
  130. package/dist/services/module-metadata.service.js +28 -0
  131. package/dist/services/module-metadata.service.js.map +1 -1
  132. package/dist/services/removed-field-migration.service.d.ts +30 -0
  133. package/dist/services/removed-field-migration.service.d.ts.map +1 -0
  134. package/dist/services/removed-field-migration.service.js +319 -0
  135. package/dist/services/removed-field-migration.service.js.map +1 -0
  136. package/dist/services/role-metadata.service.d.ts +5 -0
  137. package/dist/services/role-metadata.service.d.ts.map +1 -1
  138. package/dist/services/role-metadata.service.js +95 -33
  139. package/dist/services/role-metadata.service.js.map +1 -1
  140. package/dist/services/settings/default-settings-provider.service.d.ts +244 -0
  141. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  142. package/dist/services/settings/default-settings-provider.service.js +120 -0
  143. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  144. package/dist/services/solid-ts-morph.service.d.ts +6 -0
  145. package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
  146. package/dist/services/solid-ts-morph.service.js +43 -0
  147. package/dist/services/solid-ts-morph.service.js.map +1 -1
  148. package/dist/services/view-metadata.service.d.ts +6 -0
  149. package/dist/services/view-metadata.service.d.ts.map +1 -1
  150. package/dist/services/view-metadata.service.js +70 -8
  151. package/dist/services/view-metadata.service.js.map +1 -1
  152. package/dist/solid-core.module.d.ts.map +1 -1
  153. package/dist/solid-core.module.js +4 -0
  154. package/dist/solid-core.module.js.map +1 -1
  155. package/dist/subscribers/audit.subscriber.d.ts +5 -1
  156. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  157. package/dist/subscribers/audit.subscriber.js +76 -12
  158. package/dist/subscribers/audit.subscriber.js.map +1 -1
  159. package/dist/subscribers/computed-entity-field.subscriber.d.ts +9 -1
  160. package/dist/subscribers/computed-entity-field.subscriber.d.ts.map +1 -1
  161. package/dist/subscribers/computed-entity-field.subscriber.js +45 -11
  162. package/dist/subscribers/computed-entity-field.subscriber.js.map +1 -1
  163. package/dist/subscribers/created-by-updated-by.subscriber.d.ts.map +1 -1
  164. package/dist/subscribers/created-by-updated-by.subscriber.js.map +1 -1
  165. package/dist/subscribers/field-metadata.subscriber.d.ts.map +1 -1
  166. package/dist/subscribers/field-metadata.subscriber.js.map +1 -1
  167. package/dist/subscribers/scheduled-job.subscriber.d.ts.map +1 -1
  168. package/dist/subscribers/scheduled-job.subscriber.js +3 -2
  169. package/dist/subscribers/scheduled-job.subscriber.js.map +1 -1
  170. package/dist/subscribers/security-rule.subscriber.d.ts.map +1 -1
  171. package/dist/subscribers/security-rule.subscriber.js +5 -3
  172. package/dist/subscribers/security-rule.subscriber.js.map +1 -1
  173. package/package.json +1 -1
  174. package/src/commands/migrate-removed-fields.command.ts +81 -0
  175. package/src/controllers/datasource-introspection.controller.ts +27 -1
  176. package/src/controllers/service.controller.ts +3 -2
  177. package/src/dtos/datasource-introspection-mapping.dto.ts +15 -0
  178. package/src/entities/{legacy-common-with-id.entity.ts → legacy-common-with-generated-id.entity.ts} +1 -1
  179. package/src/helpers/command.service.ts +10 -2
  180. package/src/helpers/environment.helper.ts +11 -0
  181. package/src/helpers/field-crud-managers/IntFieldCrudManager.ts +4 -1
  182. package/src/helpers/model-metadata-helper.service.ts +8 -3
  183. package/src/helpers/nodemon-heartbeat.ts +59 -0
  184. package/src/helpers/solid-registry.ts +7 -1
  185. package/src/helpers/typeorm-db-helper.ts +15 -0
  186. package/src/index.ts +3 -2
  187. package/src/interfaces/datasource-introspection-provider.interface.ts +1 -0
  188. package/src/interfaces.ts +1 -0
  189. package/src/repository/scheduled-job.repository.ts +83 -4
  190. package/src/repository/security-rule.repository.ts +10 -4
  191. package/src/seeders/module-metadata-seeder.service.ts +1415 -264
  192. package/src/seeders/seed-data/solid-core-metadata.json +128 -34
  193. package/src/services/action-metadata.service.ts +87 -2
  194. package/src/services/api-key.service.ts +36 -36
  195. package/src/services/authentication.service.ts +46 -12
  196. package/src/services/computed-fields/entity/alpha-num-external-id-computed-field-provider.ts +11 -5
  197. package/src/services/computed-fields/entity/concat-entity-computed-field-provider.service.ts +15 -1
  198. package/src/services/crud-helper.service.ts +25 -12
  199. package/src/services/crud.service.ts +36 -11
  200. package/src/services/datasource-introspection/mssql-datasource-introspection-provider.service.ts +12 -0
  201. package/src/services/datasource-introspection/mysql-datasource-introspection-provider.service.ts +2 -0
  202. package/src/services/datasource-introspection/postgres-datasource-introspection-provider.service.ts +28 -15
  203. package/src/services/datasource-introspection.service.ts +811 -161
  204. package/src/services/import-transaction.service.ts +5 -0
  205. package/src/services/locale.service.ts +111 -3
  206. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +2 -2
  207. package/src/services/menu-item-metadata.service.ts +2 -1
  208. package/src/services/model-metadata.service.ts +109 -1
  209. package/src/services/module-metadata.service.ts +36 -0
  210. package/src/services/removed-field-migration.service.ts +334 -0
  211. package/src/services/role-metadata.service.ts +112 -49
  212. package/src/services/settings/default-settings-provider.service.ts +120 -0
  213. package/src/services/solid-ts-morph.service.ts +61 -1
  214. package/src/services/view-metadata.service.ts +83 -9
  215. package/src/solid-core.module.ts +4 -0
  216. package/src/subscribers/audit.subscriber.ts +101 -18
  217. package/src/subscribers/computed-entity-field.subscriber.ts +81 -28
  218. package/src/subscribers/created-by-updated-by.subscriber.ts +3 -0
  219. package/src/subscribers/field-metadata.subscriber.ts +3 -0
  220. package/src/subscribers/scheduled-job.subscriber.ts +3 -2
  221. package/src/subscribers/security-rule.subscriber.ts +8 -3
  222. package/dist/entities/legacy-common-with-id.entity.d.ts.map +0 -1
  223. package/dist/entities/legacy-common-with-id.entity.js.map +0 -1
  224. package/dist/entities/legacy-common.entity.d.ts.map +0 -1
  225. package/dist/entities/legacy-common.entity.js.map +0 -1
  226. /package/src/entities/{legacy-common.entity.ts → legacy-common.entity-with-existing-id.entity.ts} +0 -0
@@ -22,6 +22,10 @@ export class DatasourceIntrospectionMappingColumnDto {
22
22
  @IsString()
23
23
  fieldName: string;
24
24
 
25
+ @ApiProperty()
26
+ @IsString()
27
+ displayName: string;
28
+
25
29
  @ApiProperty()
26
30
  @IsString()
27
31
  dataType: string;
@@ -91,6 +95,11 @@ export class DatasourceIntrospectionMappingColumnDto {
91
95
  @ApiProperty()
92
96
  @IsBoolean()
93
97
  isPrimaryKey: boolean;
98
+
99
+ @ApiProperty({ required: false, type: "object", additionalProperties: true })
100
+ @IsOptional()
101
+ @IsObject()
102
+ fieldConfig?: Record<string, any>;
94
103
  }
95
104
 
96
105
  export class DatasourceIntrospectionMappingDto {
@@ -180,3 +189,9 @@ export class DatasourceIntrospectionMappingDto {
180
189
  @IsObject()
181
190
  reviewedModel?: Record<string, any>;
182
191
  }
192
+
193
+ export class DatasourceIntrospectionRunMigrationDto {
194
+ @ApiProperty()
195
+ @IsString()
196
+ datasource: string;
197
+ }
@@ -1,6 +1,6 @@
1
1
  import { Exclude, Expose } from "class-transformer";
2
2
  import { Column, Generated } from "typeorm";
3
- import { LEGACY_TABLE_FIELDS_PREFIX, LegacyCommonEntityWithExistingId } from "./legacy-common.entity";
3
+ import { LEGACY_TABLE_FIELDS_PREFIX, LegacyCommonEntityWithExistingId } from "./legacy-common.entity-with-existing-id.entity";
4
4
 
5
5
  @Exclude()
6
6
  export abstract class LegacyCommonEntityWithGeneratedId extends LegacyCommonEntityWithExistingId {
@@ -10,6 +10,10 @@ export type CommandWithArgs = {
10
10
  @Injectable()
11
11
  export class CommandService {
12
12
  private readonly logger = new Logger(CommandService.name);
13
+ private readonly ansiEscapePattern =
14
+ // Covers standard ANSI CSI, OSC, and related terminal escape sequences.
15
+ // eslint-disable-next-line no-control-regex
16
+ /[\u001B\u009B][[\]()#;?]*(?:(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]|(?:][^\u0007]*(?:\u0007|\u001B\\)))/g;
13
17
 
14
18
  /**
15
19
  * Escape an argument for Windows CMD shell
@@ -51,11 +55,11 @@ export class CommandService {
51
55
  let stderr = '';
52
56
 
53
57
  child.stdout.on('data', (data) => {
54
- stdout += data.toString();
58
+ stdout += this.stripAnsi(data.toString());
55
59
  });
56
60
 
57
61
  child.stderr.on('data', (data) => {
58
- stderr += data.toString();
62
+ stderr += this.stripAnsi(data.toString());
59
63
  });
60
64
 
61
65
  child.on('error', (error) => {
@@ -73,4 +77,8 @@ export class CommandService {
73
77
  });
74
78
  });
75
79
  }
80
+
81
+ private stripAnsi(value: string): string {
82
+ return value.replace(this.ansiEscapePattern, '');
83
+ }
76
84
  }
@@ -13,4 +13,15 @@ export function isRedisConfigured(configService: ConfigService): boolean {
13
13
  const host = configService.get<string>('REDIS_HOST');
14
14
  const port = configService.get<string>('REDIS_PORT');
15
15
  return host && port && isNumber(parseInt(port));
16
+ }
17
+
18
+ /**
19
+ * Returns true when the project is configured to use an embedded PGlite
20
+ * database with a single-connection pool. Several TypeORM subscriber and
21
+ * code-generation workarounds are only necessary (and only applied) under
22
+ * this constraint; on a regular multi-connection Postgres pool the original
23
+ * behaviour is preserved.
24
+ */
25
+ export function isEmbeddedDb(): boolean {
26
+ return process.env.DEFAULT_DATABASE_DRIVER === 'pglite';
16
27
  }
@@ -15,7 +15,10 @@ export class IntFieldCrudManager implements FieldCrudManager {
15
15
 
16
16
  validate(createDto: any): ValidationError[] {
17
17
  const fieldValue: any = createDto[this.options.fieldName];
18
- return this.applyValidations(fieldValue);
18
+ if (typeof fieldValue === 'string' && /^\d+$/.test(fieldValue.trim())) {
19
+ createDto[this.options.fieldName] = Number.parseInt(fieldValue, 10);
20
+ }
21
+ return this.applyValidations(createDto[this.options.fieldName]);
19
22
  }
20
23
 
21
24
  private applyValidations(fieldValue: any): ValidationError[] {
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common";
4
4
  import { _ } from "lodash";
5
- import { LEGACY_TABLE_FIELDS_PREFIX } from "src/entities/legacy-common.entity";
5
+ import { LEGACY_TABLE_FIELDS_PREFIX } from "src/entities/legacy-common.entity-with-existing-id.entity";
6
6
  import { LegacyTableType } from "src/enums/legacy-table-type.enum";
7
+ import { ModelMetadata } from "src/entities/model-metadata.entity";
7
8
  import { ModelMetadataRepository } from "src/repository/model-metadata.repository";
8
9
  import { SolidRegistry } from "./solid-registry";
10
+ import { EntityManager } from "typeorm";
9
11
 
10
12
  @Injectable()
11
13
  export class ModelMetadataHelperService {
@@ -143,8 +145,11 @@ export class ModelMetadataHelperService {
143
145
  return systemFieldsMetadata;
144
146
  }
145
147
 
146
- async loadFieldHierarchy(modelName: any) {
147
- const model = await this.modelMetadataRepo.findOne({
148
+ async loadFieldHierarchy(modelName: any, manager?: EntityManager) {
149
+ const repo = manager
150
+ ? manager.getRepository(ModelMetadata)
151
+ : this.modelMetadataRepo;
152
+ const model = await repo.findOne({
148
153
  where: {
149
154
  singularName: modelName,
150
155
  },
@@ -0,0 +1,59 @@
1
+ import * as fsSync from 'fs';
2
+ import * as path from 'path';
3
+
4
+ /**
5
+ * The heartbeat file is placed inside the nodemon-watched src/ directory so
6
+ * that touching it resets nodemon's restart delay. This prevents nodemon from
7
+ * restarting the API server while in-process code generation is writing files
8
+ * (which is especially important for embedded PGlite projects where the
9
+ * generate-code endpoint runs schematics in-process).
10
+ *
11
+ * The file uses a .json extension because nodemon is configured to watch
12
+ * ts,json files. It is placed at the src/ root (not inside any metadata
13
+ * directory) so it is not caught by the metadata JSON ignore rule.
14
+ */
15
+ const HEARTBEAT_FILENAME = 'solidx-nodemon-heartbeat.json';
16
+ const HEARTBEAT_INTERVAL_MS = 1500;
17
+
18
+ export interface NodemonHeartbeatHandle {
19
+ stop: () => void;
20
+ }
21
+
22
+ /**
23
+ * Starts a heartbeat that touches a file in the nodemon-watched directory
24
+ * every ~1.5 s. Call `stop()` after the response has been flushed to allow
25
+ * nodemon to restart.
26
+ *
27
+ * @param srcDir Absolute path to the `src/` directory nodemon watches.
28
+ */
29
+ export function startNodemonHeartbeat(srcDir: string): NodemonHeartbeatHandle {
30
+ const filePath = path.join(srcDir, HEARTBEAT_FILENAME);
31
+ let stopped = false;
32
+
33
+ const touch = () => {
34
+ try {
35
+ fsSync.writeFileSync(filePath, String(Date.now()));
36
+ } catch {
37
+ // best-effort — if the file can't be written, nodemon will simply
38
+ // restart as usual, which is the safe fallback.
39
+ }
40
+ };
41
+
42
+ // Write immediately so the delay resets before any schematic output lands.
43
+ touch();
44
+
45
+ const timer = setInterval(touch, HEARTBEAT_INTERVAL_MS);
46
+
47
+ return {
48
+ stop() {
49
+ if (stopped) return;
50
+ stopped = true;
51
+ clearInterval(timer);
52
+ try {
53
+ fsSync.unlinkSync(filePath);
54
+ } catch {
55
+ // file may not exist — ignore
56
+ }
57
+ },
58
+ };
59
+ }
@@ -8,7 +8,7 @@ import { Locale } from 'src/entities/locale.entity';
8
8
  import { SecurityRule } from 'src/entities/security-rule.entity';
9
9
  import { IScheduledJob } from 'src/services/scheduled-jobs/scheduled-job.interface';
10
10
  import { IDashboardWidgetDataProvider, IErrorCodeProvider, ISecurityRuleConfigProvider, ISelectionProvider, ISelectionProviderContext, ISolidDatabaseModule } from "../interfaces";
11
- import { ObjectLiteral } from 'typeorm';
11
+ import { EntityManager, ObjectLiteral } from 'typeorm';
12
12
 
13
13
  type ControllerMetadata = {
14
14
  name: string;
@@ -49,6 +49,12 @@ export interface TypeOrmEventContext {
49
49
  metadataName?: string;
50
50
  updatedColumns?: string[];
51
51
  updatedRelations?: string[];
52
+ // Transaction-bound EntityManager from the originating TypeORM event. Providers that
53
+ // need to query the DB during a pre-compute hook must use this (not an injected
54
+ // DataSource/EntityManager) so the query runs on the active transaction's connection.
55
+ // On single-threaded embedded engines (PGlite), a second connection mid-transaction
56
+ // can deadlock.
57
+ manager?: EntityManager;
52
58
  }
53
59
 
54
60
  export interface ComputedFieldMetadata<TContext = any> {
@@ -45,6 +45,21 @@ const solidCoreDbType: DatasourceType =
45
45
  ? (process.env.SOLID_CORE_DB_TYPE as DatasourceType)
46
46
  : DatasourceType.postgres;
47
47
 
48
+ // Must use ANSI CAST() syntax — not Postgres-specific `::text` — because TypeORM's
49
+ // replacePropertyNamesForTheWholeQuery regex treats ':' as part of the property name,
50
+ // so `alias.col::text` is parsed as property `col::text` (no match → no column substitution).
51
+ export function buildCastToText(driver: string, colExpr: string): string {
52
+ switch (driver) {
53
+ case DatasourceType.mssql:
54
+ return `CAST(${colExpr} AS NVARCHAR(MAX))`;
55
+ case DatasourceType.mysql:
56
+ case DatasourceType.mariadb:
57
+ return `CAST(${colExpr} AS CHAR)`;
58
+ default:
59
+ return `CAST(${colExpr} AS TEXT)`;
60
+ }
61
+ }
62
+
48
63
  export function getColumnType(solidType: string): ColumnOptions {
49
64
  switch (solidType) {
50
65
  case "longText":
package/src/index.ts CHANGED
@@ -119,8 +119,8 @@ export * from './dtos/update-dashboard-user-layout.dto'
119
119
 
120
120
  export * from './entities/action-metadata.entity'
121
121
  export * from './entities/common.entity'
122
- export * from './entities/legacy-common.entity'
123
- export * from './entities/legacy-common-with-id.entity'
122
+ export * from './entities/legacy-common.entity-with-existing-id.entity'
123
+ export * from './entities/legacy-common-with-generated-id.entity'
124
124
  export * from './entities/email-attachment.entity'
125
125
  export * from './entities/email-template.entity'
126
126
  export * from './entities/field-metadata.entity'
@@ -171,6 +171,7 @@ export * from './guards/permissions.guard'
171
171
  export * from './guards/roles.guard'
172
172
 
173
173
  export * from './helpers/command.service'
174
+ export * from './helpers/nodemon-heartbeat'
174
175
  export * from './helpers/module.helper'
175
176
  export * from './helpers/schematic.service'
176
177
  export * from './helpers/solid-registry'
@@ -11,6 +11,7 @@ export type DatasourceIntrospectionColumn = {
11
11
  columnName: string;
12
12
  dataType: string;
13
13
  isAutoGenerated: boolean;
14
+ isPrimaryKey: boolean;
14
15
  isNullable: boolean;
15
16
  defaultValue: string | null;
16
17
  characterMaximumLength: number | null;
package/src/interfaces.ts CHANGED
@@ -89,6 +89,7 @@ export interface SettingDefinition<T = any> {
89
89
  encrypted?: boolean;
90
90
  label?: string;
91
91
  description?: string;
92
+ helpText?: string;
92
93
  placeholder?: string;
93
94
  group?: string;
94
95
  sortOrder?: number;
@@ -3,7 +3,7 @@ import { CreateScheduledJobDto } from "src/dtos/create-scheduled-job.dto";
3
3
  import { ModuleMetadata } from "src/entities/module-metadata.entity";
4
4
  import { ScheduledJob } from "src/entities/scheduled-job.entity";
5
5
  import { RequestContextService } from "src/services/request-context.service";
6
- import { DataSource } from "typeorm";
6
+ import { DataSource, EntityManager } from "typeorm";
7
7
  import { SecurityRuleRepository } from "./security-rule.repository";
8
8
  import { SolidBaseRepository } from "./solid-base.repository";
9
9
 
@@ -34,21 +34,92 @@ export class ScheduledJobRepository extends SolidBaseRepository<ScheduledJob> {
34
34
  super(ScheduledJob, dataSource, requestContextService, securityRuleRepository);
35
35
  }
36
36
 
37
+ private normalizeComparableValue(value: any): any {
38
+ if (value instanceof Date) {
39
+ return value.toISOString();
40
+ }
41
+
42
+ if (typeof value === "string") {
43
+ const trimmedValue = value.trim();
44
+ if (
45
+ (trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) ||
46
+ (trimmedValue.startsWith("[") && trimmedValue.endsWith("]"))
47
+ ) {
48
+ try {
49
+ return this.normalizeComparableValue(JSON.parse(trimmedValue));
50
+ } catch {
51
+ return value;
52
+ }
53
+ }
54
+ return value;
55
+ }
56
+
57
+ if (Array.isArray(value)) {
58
+ return value.map((item) => this.normalizeComparableValue(item));
59
+ }
60
+
61
+ if (value && typeof value === "object") {
62
+ return Object.keys(value)
63
+ .sort()
64
+ .reduce((result, key) => {
65
+ result[key] = this.normalizeComparableValue(value[key]);
66
+ return result;
67
+ }, {} as Record<string, any>);
68
+ }
69
+
70
+ return value;
71
+ }
72
+
73
+ private areValuesDifferent(currentValue: any, nextValue: any): boolean {
74
+ const normalizedCurrentValue = this.normalizeComparableValue(currentValue);
75
+ const normalizedNextValue = this.normalizeComparableValue(nextValue);
76
+
77
+ if (
78
+ (normalizedCurrentValue && typeof normalizedCurrentValue === "object") ||
79
+ (normalizedNextValue && typeof normalizedNextValue === "object")
80
+ ) {
81
+ return JSON.stringify(normalizedCurrentValue ?? null) !== JSON.stringify(normalizedNextValue ?? null);
82
+ }
83
+
84
+ return normalizedCurrentValue !== normalizedNextValue;
85
+ }
86
+
87
+ private hasChanges(existing: ScheduledJob, dto: CreateScheduledJobDto, moduleEntity: ModuleMetadata): boolean {
88
+ return existing.scheduleName !== dto.scheduleName
89
+ || existing.isActive !== dto.isActive
90
+ || this.areValuesDifferent(existing.frequency, dto.frequency)
91
+ || this.areValuesDifferent(existing.startTime, dto.startTime)
92
+ || this.areValuesDifferent(existing.endTime, dto.endTime)
93
+ || this.areValuesDifferent(existing.startDate, dto.startDate)
94
+ || this.areValuesDifferent(existing.endDate, dto.endDate)
95
+ || this.areValuesDifferent(existing.dayOfMonth, dto.dayOfMonth)
96
+ || this.areValuesDifferent(existing.dayOfWeek, dto.dayOfWeek)
97
+ || this.areValuesDifferent(existing.job, dto.job)
98
+ || this.areValuesDifferent(existing.cronExpression, dto.cronExpression)
99
+ || existing.module?.id !== moduleEntity.id;
100
+ }
101
+
37
102
  /**
38
103
  * Converts an entity to a plain DTO object.
104
+ * When manager is provided (e.g. from a TypeORM subscriber event), the
105
+ * fallback relation load uses it so the query runs on the active
106
+ * transaction's connection instead of the default DataSource pool.
39
107
  */
40
- async toDto(scheduledJob: ScheduledJob): Promise<CreateScheduledJobDto> {
108
+ async toDto(scheduledJob: ScheduledJob, manager?: EntityManager): Promise<CreateScheduledJobDto> {
41
109
  let populatedScheduledJob: ScheduledJob = scheduledJob;
42
110
  // If the scheduled job does not have the module relation loaded, load it
43
111
  if (!scheduledJob.module) {
44
- populatedScheduledJob = await this.findOne({
112
+ const repo = manager
113
+ ? manager.getRepository(ScheduledJob)
114
+ : this;
115
+ populatedScheduledJob = await repo.findOne({
45
116
  where: {
46
117
  id: scheduledJob.id,
47
118
  },
48
119
  relations: {
49
120
  module: true,
50
121
  },
51
- });
122
+ }) ?? scheduledJob;
52
123
  }
53
124
 
54
125
  if (!populatedScheduledJob.module) {
@@ -91,9 +162,17 @@ export class ScheduledJobRepository extends SolidBaseRepository<ScheduledJob> {
91
162
  };
92
163
  const existing = await this.findOne({
93
164
  where: { scheduleName: dto.scheduleName },
165
+ relations: {
166
+ module: true,
167
+ },
94
168
  });
95
169
 
96
170
  if (existing) {
171
+ if (!this.hasChanges(existing, dto, moduleEntity)) {
172
+ this.logger.debug(`Skipping scheduled job upsert for ${dto.scheduleName}; no changes detected.`);
173
+ return existing;
174
+ }
175
+
97
176
  const merged = this.merge(existing, jobData);
98
177
  this.logger.debug(`Updating scheduled job: ${dto.scheduleName}`);
99
178
  return this.save(merged);
@@ -9,7 +9,7 @@ import { SecurityRule } from 'src/entities/security-rule.entity';
9
9
  import { SolidRegistry } from 'src/helpers/solid-registry';
10
10
  import { ActiveUserData } from 'src/interfaces/active-user-data.interface';
11
11
  import { CrudHelperService } from 'src/services/crud-helper.service';
12
- import { Brackets, DataSource, SelectQueryBuilder } from 'typeorm';
12
+ import { Brackets, DataSource, EntityManager, SelectQueryBuilder } from 'typeorm';
13
13
  import { SolidBaseRepository } from './solid-base.repository';
14
14
 
15
15
  @Injectable()
@@ -84,12 +84,18 @@ export class SecurityRuleRepository extends SolidBaseRepository<SecurityRule> {
84
84
  return configString.replace('$activeUserId', activeUser.sub.toString());
85
85
  }
86
86
 
87
- async toDto(securityRule: SecurityRule): Promise<UpdateSecurityRuleDto> {
87
+ async toDto(securityRule: SecurityRule, manager?: EntityManager): Promise<UpdateSecurityRuleDto> {
88
88
  // load the role and model relations for the security rule
89
89
  let populatedSecurityRule: SecurityRule = securityRule;
90
90
  // If the security rule does not have the role and model relations loaded, load them
91
91
  if (!securityRule.role || !securityRule.modelMetadata) {
92
- populatedSecurityRule = await this.findOne({
92
+ // When a manager is provided (e.g. from a TypeORM subscriber event),
93
+ // use it so the query runs on the active transaction's connection
94
+ // instead of trying to acquire a second connection from the pool.
95
+ const repo = manager
96
+ ? manager.getRepository(SecurityRule)
97
+ : this;
98
+ populatedSecurityRule = await repo.findOne({
93
99
  where: {
94
100
  id: securityRule.id,
95
101
  },
@@ -97,7 +103,7 @@ export class SecurityRuleRepository extends SolidBaseRepository<SecurityRule> {
97
103
  role: true,
98
104
  modelMetadata: true,
99
105
  },
100
- });
106
+ }) ?? securityRule;
101
107
  }
102
108
 
103
109
  return {