@solidxai/core 0.1.8-beta.1 → 0.1.8-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 (202) hide show
  1. package/README.md +197 -0
  2. package/dist/controllers/authentication.controller.d.ts +32 -2
  3. package/dist/controllers/authentication.controller.d.ts.map +1 -1
  4. package/dist/controllers/authentication.controller.js +80 -3
  5. package/dist/controllers/authentication.controller.js.map +1 -1
  6. package/dist/dtos/create-api-key.dto.d.ts +5 -0
  7. package/dist/dtos/create-api-key.dto.d.ts.map +1 -0
  8. package/dist/dtos/create-api-key.dto.js +34 -0
  9. package/dist/dtos/create-api-key.dto.js.map +1 -0
  10. package/dist/dtos/post-chatter-message.dto.d.ts +1 -0
  11. package/dist/dtos/post-chatter-message.dto.d.ts.map +1 -1
  12. package/dist/dtos/post-chatter-message.dto.js +6 -1
  13. package/dist/dtos/post-chatter-message.dto.js.map +1 -1
  14. package/dist/dtos/register-private.dto.d.ts +3 -5
  15. package/dist/dtos/register-private.dto.d.ts.map +1 -1
  16. package/dist/dtos/register-private.dto.js +6 -18
  17. package/dist/dtos/register-private.dto.js.map +1 -1
  18. package/dist/dtos/sso-exchange.dto.d.ts +4 -0
  19. package/dist/dtos/sso-exchange.dto.d.ts.map +1 -0
  20. package/dist/dtos/sso-exchange.dto.js +26 -0
  21. package/dist/dtos/sso-exchange.dto.js.map +1 -0
  22. package/dist/dtos/update-api-key.dto.d.ts +4 -0
  23. package/dist/dtos/update-api-key.dto.d.ts.map +1 -0
  24. package/dist/dtos/update-api-key.dto.js +28 -0
  25. package/dist/dtos/update-api-key.dto.js.map +1 -0
  26. package/dist/entities/agent-event.entity.d.ts +3 -12
  27. package/dist/entities/agent-event.entity.d.ts.map +1 -1
  28. package/dist/entities/agent-event.entity.js +21 -46
  29. package/dist/entities/agent-event.entity.js.map +1 -1
  30. package/dist/entities/agent-session.entity.d.ts +2 -11
  31. package/dist/entities/agent-session.entity.d.ts.map +1 -1
  32. package/dist/entities/agent-session.entity.js +15 -40
  33. package/dist/entities/agent-session.entity.js.map +1 -1
  34. package/dist/entities/field-metadata.entity.js +1 -1
  35. package/dist/entities/field-metadata.entity.js.map +1 -1
  36. package/dist/entities/legacy-common.entity.d.ts +9 -9
  37. package/dist/entities/legacy-common.entity.d.ts.map +1 -1
  38. package/dist/entities/legacy-common.entity.js +7 -7
  39. package/dist/entities/legacy-common.entity.js.map +1 -1
  40. package/dist/entities/setting.entity.d.ts +1 -0
  41. package/dist/entities/setting.entity.d.ts.map +1 -1
  42. package/dist/entities/setting.entity.js +5 -1
  43. package/dist/entities/setting.entity.js.map +1 -1
  44. package/dist/entities/sms-template.entity.d.ts.map +1 -1
  45. package/dist/entities/sms-template.entity.js +2 -1
  46. package/dist/entities/sms-template.entity.js.map +1 -1
  47. package/dist/entities/user-api-key.entity.d.ts +12 -0
  48. package/dist/entities/user-api-key.entity.d.ts.map +1 -0
  49. package/dist/entities/user-api-key.entity.js +62 -0
  50. package/dist/entities/user-api-key.entity.js.map +1 -0
  51. package/dist/entities/user.entity.d.ts +3 -0
  52. package/dist/entities/user.entity.d.ts.map +1 -1
  53. package/dist/entities/user.entity.js +12 -1
  54. package/dist/entities/user.entity.js.map +1 -1
  55. package/dist/enums/auth-type.enum.d.ts +2 -1
  56. package/dist/enums/auth-type.enum.d.ts.map +1 -1
  57. package/dist/enums/auth-type.enum.js +2 -1
  58. package/dist/enums/auth-type.enum.js.map +1 -1
  59. package/dist/guards/api-key.guard.d.ts +11 -0
  60. package/dist/guards/api-key.guard.d.ts.map +1 -0
  61. package/dist/guards/api-key.guard.js +43 -0
  62. package/dist/guards/api-key.guard.js.map +1 -0
  63. package/dist/guards/authentication.guard.d.ts +4 -2
  64. package/dist/guards/authentication.guard.d.ts.map +1 -1
  65. package/dist/guards/authentication.guard.js +7 -3
  66. package/dist/guards/authentication.guard.js.map +1 -1
  67. package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
  68. package/dist/helpers/bootstrap.helper.js +12 -1
  69. package/dist/helpers/bootstrap.helper.js.map +1 -1
  70. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.d.ts.map +1 -1
  71. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js +15 -6
  72. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js.map +1 -1
  73. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  74. package/dist/helpers/typeorm-db-helper.js +9 -0
  75. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/interfaces.d.ts +12 -0
  81. package/dist/interfaces.d.ts.map +1 -1
  82. package/dist/interfaces.js.map +1 -1
  83. package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts +1 -1
  84. package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts.map +1 -1
  85. package/dist/jobs/database/chatter-queue-publisher-database.service.js.map +1 -1
  86. package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts +1 -1
  87. package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts.map +1 -1
  88. package/dist/jobs/database/chatter-queue-subscriber-database.service.js.map +1 -1
  89. package/dist/jobs/rabbitmq/chatter-queue-publisher.service.d.ts +1 -12
  90. package/dist/jobs/rabbitmq/chatter-queue-publisher.service.d.ts.map +1 -1
  91. package/dist/jobs/rabbitmq/chatter-queue-publisher.service.js.map +1 -1
  92. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts +1 -1
  93. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts.map +1 -1
  94. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.js.map +1 -1
  95. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts +1 -1
  96. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts.map +1 -1
  97. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.js.map +1 -1
  98. package/dist/repository/user-api-key.repository.d.ts +12 -0
  99. package/dist/repository/user-api-key.repository.d.ts.map +1 -0
  100. package/dist/repository/user-api-key.repository.js +34 -0
  101. package/dist/repository/user-api-key.repository.js.map +1 -0
  102. package/dist/seeders/module-test-data.service.d.ts +5 -0
  103. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  104. package/dist/seeders/module-test-data.service.js +131 -4
  105. package/dist/seeders/module-test-data.service.js.map +1 -1
  106. package/dist/seeders/seed-data/solid-core-metadata.json +287 -197
  107. package/dist/services/api-key.service.d.ts +20 -0
  108. package/dist/services/api-key.service.d.ts.map +1 -0
  109. package/dist/services/api-key.service.js +98 -0
  110. package/dist/services/api-key.service.js.map +1 -0
  111. package/dist/services/authentication.service.d.ts +19 -1
  112. package/dist/services/authentication.service.d.ts.map +1 -1
  113. package/dist/services/authentication.service.js +31 -5
  114. package/dist/services/authentication.service.js.map +1 -1
  115. package/dist/services/chatter-message.service.d.ts.map +1 -1
  116. package/dist/services/chatter-message.service.js +6 -0
  117. package/dist/services/chatter-message.service.js.map +1 -1
  118. package/dist/services/encryption.service.d.ts +8 -0
  119. package/dist/services/encryption.service.d.ts.map +1 -0
  120. package/dist/services/encryption.service.js +75 -0
  121. package/dist/services/encryption.service.js.map +1 -0
  122. package/dist/services/export-transaction.service.d.ts.map +1 -1
  123. package/dist/services/export-transaction.service.js +0 -23
  124. package/dist/services/export-transaction.service.js.map +1 -1
  125. package/dist/services/field-metadata.service.d.ts +1 -3
  126. package/dist/services/field-metadata.service.d.ts.map +1 -1
  127. package/dist/services/field-metadata.service.js +6 -13
  128. package/dist/services/field-metadata.service.js.map +1 -1
  129. package/dist/services/file/disk-file.service.d.ts +1 -0
  130. package/dist/services/file/disk-file.service.d.ts.map +1 -1
  131. package/dist/services/file/disk-file.service.js +11 -3
  132. package/dist/services/file/disk-file.service.js.map +1 -1
  133. package/dist/services/media.service.d.ts +0 -1
  134. package/dist/services/media.service.d.ts.map +1 -1
  135. package/dist/services/media.service.js +10 -11
  136. package/dist/services/media.service.js.map +1 -1
  137. package/dist/services/setting.service.d.ts +1 -0
  138. package/dist/services/setting.service.d.ts.map +1 -1
  139. package/dist/services/setting.service.js +35 -7
  140. package/dist/services/setting.service.js.map +1 -1
  141. package/dist/services/settings/default-settings-provider.service.d.ts +12 -0
  142. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  143. package/dist/services/settings/default-settings-provider.service.js +7 -3
  144. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  145. package/dist/services/sso-code-storage.service.d.ts +15 -0
  146. package/dist/services/sso-code-storage.service.d.ts.map +1 -0
  147. package/dist/services/sso-code-storage.service.js +47 -0
  148. package/dist/services/sso-code-storage.service.js.map +1 -0
  149. package/dist/services/user.service.d.ts.map +1 -1
  150. package/dist/services/user.service.js +3 -2
  151. package/dist/services/user.service.js.map +1 -1
  152. package/dist/solid-core.module.d.ts.map +1 -1
  153. package/dist/solid-core.module.js +10 -0
  154. package/dist/solid-core.module.js.map +1 -1
  155. package/dist/subscribers/audit.subscriber.d.ts +1 -1
  156. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  157. package/dist/subscribers/audit.subscriber.js.map +1 -1
  158. package/package.json +1 -1
  159. package/src/controllers/authentication.controller.ts +59 -3
  160. package/src/dtos/create-api-key.dto.ts +14 -0
  161. package/src/dtos/post-chatter-message.dto.ts +4 -0
  162. package/src/dtos/register-private.dto.ts +5 -14
  163. package/src/dtos/sso-exchange.dto.ts +7 -0
  164. package/src/dtos/update-api-key.dto.ts +9 -0
  165. package/src/entities/agent-event.entity.ts +21 -55
  166. package/src/entities/agent-session.entity.ts +15 -47
  167. package/src/entities/field-metadata.entity.ts +1 -1
  168. package/src/entities/legacy-common.entity.ts +15 -15
  169. package/src/entities/setting.entity.ts +3 -0
  170. package/src/entities/sms-template.entity.ts +3 -2
  171. package/src/entities/user-api-key.entity.ts +37 -0
  172. package/src/entities/user.entity.ts +8 -0
  173. package/src/enums/auth-type.enum.ts +1 -0
  174. package/src/guards/api-key.guard.ts +32 -0
  175. package/src/guards/authentication.guard.ts +6 -3
  176. package/src/helpers/bootstrap.helper.ts +16 -1
  177. package/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts +17 -6
  178. package/src/helpers/typeorm-db-helper.ts +11 -0
  179. package/src/index.ts +2 -0
  180. package/src/interfaces.ts +16 -0
  181. package/src/jobs/database/chatter-queue-publisher-database.service.ts +1 -1
  182. package/src/jobs/database/chatter-queue-subscriber-database.service.ts +1 -1
  183. package/src/jobs/rabbitmq/chatter-queue-publisher.service.ts +1 -15
  184. package/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts +1 -1
  185. package/src/jobs/redis/chatter-queue-subscriber-redis.service.ts +1 -1
  186. package/src/repository/user-api-key.repository.ts +17 -0
  187. package/src/seeders/module-test-data.service.ts +165 -6
  188. package/src/seeders/seed-data/solid-core-metadata.json +287 -197
  189. package/src/services/api-key.service.ts +111 -0
  190. package/src/services/authentication.service.ts +35 -3
  191. package/src/services/chatter-message.service.ts +7 -0
  192. package/src/services/encryption.service.ts +43 -0
  193. package/src/services/export-transaction.service.ts +0 -26
  194. package/src/services/field-metadata.service.ts +5 -12
  195. package/src/services/file/disk-file.service.ts +15 -7
  196. package/src/services/media.service.ts +12 -51
  197. package/src/services/setting.service.ts +38 -9
  198. package/src/services/settings/default-settings-provider.service.ts +7 -3
  199. package/src/services/sso-code-storage.service.ts +36 -0
  200. package/src/services/user.service.ts +3 -2
  201. package/src/solid-core.module.ts +10 -0
  202. package/src/subscribers/audit.subscriber.ts +1 -1
@@ -90,6 +90,21 @@ export async function bootstrapSolidApp(
90
90
  // Security headers
91
91
  app.use(helmet(buildDefaultSecurityHeaderOptions()));
92
92
 
93
+ // Nest's Swagger UI HTML injects inline styles; keep CSP strict elsewhere.
94
+ const isSwaggerPath = (path: string) =>
95
+ path === '/docs' ||
96
+ path === '/docs/' ||
97
+ path.startsWith('/docs/') ||
98
+ path === '/docs-json' ||
99
+ path === '/docs-yaml';
100
+
101
+ app.use((req: Request, res: Response, next: NextFunction) => {
102
+ if (isSwaggerPath(req.path)) {
103
+ res.removeHeader('Content-Security-Policy');
104
+ }
105
+ next();
106
+ });
107
+
93
108
  // Permissions-Policy header
94
109
  app.use((_req: Request, res: Response, next: NextFunction) => {
95
110
  res.setHeader('Permissions-Policy', buildPermissionsPolicyHeader(permissionsPolicyOverrides));
@@ -127,7 +142,7 @@ export async function bootstrapSolidApp(
127
142
 
128
143
  // Swagger
129
144
  if (swagger !== false) {
130
- const { title = 'Solid Starters', description = 'Solid Starters API', version = '1.0' } = swagger;
145
+ const { title = process.env.SOLID_APP_NAME, description = process.env.SOLID_APP_DESCRIPTION, version = '1.0' } = swagger;
131
146
  const swaggerConfig = new DocumentBuilder()
132
147
  .setTitle(title)
133
148
  .setDescription(description)
@@ -128,14 +128,25 @@ export class SelectionDynamicFieldCrudManager implements FieldCrudManager {
128
128
  }
129
129
 
130
130
  private providerInstance<T extends ISelectionProviderContext>(selectionDynamicProvider: string): ISelectionProvider<T> {
131
- const provider = this.options.discoveryService
132
- .getProviders()
133
- .filter((provider) => provider.name === selectionDynamicProvider)
134
- .pop();
135
- if (!provider) {
131
+ const providers = this.options.discoveryService.getProviders();
132
+
133
+ const byToken = providers.find((p) => p.name === selectionDynamicProvider);
134
+ if (byToken) {
135
+ return byToken.instance as ISelectionProvider<T>;
136
+ }
137
+
138
+ const byName = providers.find((p) => {
139
+ try {
140
+ return typeof p.instance?.name === 'function' && p.instance.name() === selectionDynamicProvider;
141
+ } catch {
142
+ return false;
143
+ }
144
+ });
145
+
146
+ if (!byName) {
136
147
  throw new Error(`Provider for ${selectionDynamicProvider} not found`);
137
148
  }
138
- return provider.instance as ISelectionProvider<T>;
149
+ return byName.instance as ISelectionProvider<T>;
139
150
  }
140
151
 
141
152
  private isApplyRequiredValidation(): boolean {
@@ -32,6 +32,14 @@ const SIMPLE_JSON_LARGE_TEXT_MAP: Record<DatasourceType, ColumnOptions> = {
32
32
  [DatasourceType.oracle]: { type: "clob" },
33
33
  };
34
34
 
35
+ const DECIMAL_MAP: Record<DatasourceType, ColumnOptions> = {
36
+ [DatasourceType.postgres]: { type: "float4" },
37
+ [DatasourceType.mssql]: { type: "float" },
38
+ [DatasourceType.mysql]: { type: "float" },
39
+ [DatasourceType.mariadb]: { type: "float" },
40
+ [DatasourceType.oracle]: { type: "float" },
41
+ };
42
+
35
43
  const solidCoreDbType: DatasourceType =
36
44
  Object.values(DatasourceType).includes(process.env.SOLID_CORE_DB_TYPE as DatasourceType)
37
45
  ? (process.env.SOLID_CORE_DB_TYPE as DatasourceType)
@@ -46,6 +54,9 @@ export function getColumnType(solidType: string): ColumnOptions {
46
54
  case "simpleJsonLargeText":
47
55
  return SIMPLE_JSON_LARGE_TEXT_MAP[solidCoreDbType];
48
56
 
57
+ case "decimal":
58
+ return DECIMAL_MAP[solidCoreDbType];
59
+
49
60
  default:
50
61
  return {};
51
62
  }
package/src/index.ts CHANGED
@@ -126,6 +126,7 @@ export * from './entities/permission-metadata.entity'
126
126
  export * from './entities/role-metadata.entity'
127
127
  export * from './entities/sms-template.entity'
128
128
  export * from './entities/user.entity'
129
+ export * from './entities/user-api-key.entity'
129
130
  export * from './entities/view-metadata.entity'
130
131
  export * from './entities/setting.entity'
131
132
  export * from './entities/saved-filters.entity'
@@ -314,6 +315,7 @@ export * from './services/user.service'
314
315
  export * from './services/view-metadata.service'
315
316
  export * from './services/whatsapp/Msg91WhatsappService' //rename
316
317
  export * from './services/setting.service'
318
+ export * from './services/encryption.service'
317
319
  export * from './services/info.service'
318
320
  export * from './controllers/info.controller'
319
321
  export * from './services/settings/default-settings-provider.service'
package/src/interfaces.ts CHANGED
@@ -70,6 +70,7 @@ export interface SettingDefinition<T = any> {
70
70
  key: string;
71
71
  value: T;
72
72
  level: SettingLevel;
73
+ encrypted?: boolean;
73
74
  }
74
75
 
75
76
  // solid-core/settings/settings-provider.interface.ts
@@ -395,3 +396,18 @@ export interface AwsS3Config {
395
396
 
396
397
  // Prevents inference so callers must provide explicit type arguments; reusable for other APIs.
397
398
  export type NoInfer<T> = [T][T extends any ? 0 : never];
399
+
400
+ export type AuditEventType = 'insert' | 'update' | 'delete';
401
+
402
+ export interface AuditQueuePayload {
403
+ eventType: AuditEventType;
404
+ modelName: string;
405
+ entityId: string | number | null;
406
+ occurredAt: string;
407
+ after?: any;
408
+ before?: any;
409
+ updatedColumnNames?: string[];
410
+ userId?: number | null;
411
+ }
412
+
413
+
@@ -4,7 +4,7 @@ import { DatabasePublisher } from 'src/services/queues/database-publisher.servic
4
4
  import { MqMessageQueueService } from '../../services/mq-message-queue.service';
5
5
  import { MqMessageService } from '../../services/mq-message.service';
6
6
  import { QueuesModuleOptions } from "../../interfaces";
7
- import { AuditQueuePayload } from '../rabbitmq/chatter-queue-publisher.service';
7
+ import { AuditQueuePayload } from '../../interfaces';
8
8
  import chatterQueueOptionsDatabase from './chatter-queue-options-database';
9
9
 
10
10
  @Injectable()
@@ -6,7 +6,7 @@ import { MqMessageService } from '../../services/mq-message.service';
6
6
  import { MqMessageQueueService } from '../../services/mq-message-queue.service';
7
7
  import { QueuesModuleOptions } from "../../interfaces";
8
8
  import { PollerService } from 'src/services/poller.service';
9
- import { AuditQueuePayload } from '../rabbitmq/chatter-queue-publisher.service';
9
+ import { AuditQueuePayload } from '../../interfaces';
10
10
  import { ChatterMessageService } from 'src/services/chatter-message.service';
11
11
  import chatterQueueOptionsDatabase from './chatter-queue-options-database';
12
12
 
@@ -4,21 +4,7 @@ import { RabbitMqPublisher } from 'src/services/queues/rabbitmq-publisher.servic
4
4
  import chatterQueueOptions from './chatter-queue-options';
5
5
  import { MqMessageQueueService } from '../../services/mq-message-queue.service';
6
6
  import { MqMessageService } from '../../services/mq-message.service';
7
- import { QueuesModuleOptions } from "../../interfaces";
8
-
9
-
10
- export type AuditEventType = 'insert' | 'update' | 'delete';
11
-
12
- export interface AuditQueuePayload {
13
- eventType: AuditEventType;
14
- modelName: string; // TypeORM entity class name (e.g. 'Order')
15
- entityId: string | number | null;
16
- occurredAt: string; // ISO timestamp, captured at event time
17
- after?: any; // entity state after operation (insert/update)
18
- before?: any; // entity state before operation (update/delete)
19
- updatedColumnNames?: string[]; // propertyNames of changed columns (update only)
20
- userId?: number | null; // active user captured at event time
21
- }
7
+ import { AuditQueuePayload, QueuesModuleOptions } from "../../interfaces";
22
8
 
23
9
  @Injectable()
24
10
  export class ChatterQueuePublisherRabbitmq extends RabbitMqPublisher<AuditQueuePayload> {
@@ -6,7 +6,7 @@ import { MqMessageService } from '../../services/mq-message.service';
6
6
  import { MqMessageQueueService } from '../../services/mq-message-queue.service';
7
7
  import { QueuesModuleOptions } from "../../interfaces";
8
8
  import chatterQueueOptions from './chatter-queue-options';
9
- import { AuditQueuePayload } from './chatter-queue-publisher.service';
9
+ import { AuditQueuePayload } from '../../interfaces';
10
10
  import { ChatterMessageService } from 'src/services/chatter-message.service';
11
11
 
12
12
  @Injectable()
@@ -6,7 +6,7 @@ import chatterQueueConfig from './chatter-queue-options-redis';
6
6
  import { MqMessageService } from '../../services/mq-message.service';
7
7
  import { MqMessageQueueService } from '../../services/mq-message-queue.service';
8
8
  import { QueuesModuleOptions } from "../../interfaces";
9
- import { AuditQueuePayload } from '../rabbitmq/chatter-queue-publisher.service';
9
+ import { AuditQueuePayload } from '../../interfaces';
10
10
  import { ChatterMessageService } from '../../services/chatter-message.service';
11
11
 
12
12
  @Injectable()
@@ -0,0 +1,17 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { UserApiKey } from 'src/entities/user-api-key.entity';
3
+ import { RequestContextService } from 'src/services/request-context.service';
4
+ import { DataSource } from 'typeorm';
5
+ import { SecurityRuleRepository } from './security-rule.repository';
6
+ import { SolidBaseRepository } from './solid-base.repository';
7
+
8
+ @Injectable()
9
+ export class UserApiKeyRepository extends SolidBaseRepository<UserApiKey> {
10
+ constructor(
11
+ readonly dataSource: DataSource,
12
+ readonly requestContextService: RequestContextService,
13
+ readonly securityRuleRepository: SecurityRuleRepository,
14
+ ) {
15
+ super(UserApiKey, dataSource, requestContextService, securityRuleRepository);
16
+ }
17
+ }
@@ -2,15 +2,19 @@ import { Injectable, Logger } from '@nestjs/common';
2
2
  import { DiscoveryService, ModuleRef } from '@nestjs/core';
3
3
  import { getDataSourceToken } from '@nestjs/typeorm';
4
4
  import { classify } from '@angular-devkit/core/src/utils/strings';
5
- import { DataSource } from 'typeorm';
5
+ import { DataSource, EntityManager } from 'typeorm';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
 
9
9
  import solidCoreMetadata from './seed-data/solid-core-metadata.json';
10
10
  import { CreateModuleMetadataDto } from 'src/dtos/create-module-metadata.dto';
11
11
  import { CreateModelMetadataDto } from 'src/dtos/create-model-metadata.dto';
12
+ import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
12
13
  import { getDynamicModuleNamesBasedOnMetadata } from 'src/helpers/module.helper';
13
14
  import { SolidRegistry } from 'src/helpers/solid-registry';
15
+ import { MediaRepository } from 'src/repository/media.repository';
16
+ import { ModelMetadataService } from 'src/services/model-metadata.service';
17
+ import { getMediaStorageProvider } from 'src/services/mediaStorageProviders';
14
18
 
15
19
  @Injectable()
16
20
  export class ModuleTestDataService {
@@ -185,7 +189,7 @@ export class ModuleTestDataService {
185
189
  throw new Error('Module metadata missing from test data payload.');
186
190
  }
187
191
 
188
- // console.log(JSON.stringify(moduleMetadata, null, 2));
192
+ // console.log(JSON.stringify(moduleMetadata, null, 2));
189
193
 
190
194
  const testingData: Array<{ modelUserKey: string; data: Record<string, any> }> = overallMetadata?.testing?.data ?? [];
191
195
  if (testingData.length === 0) {
@@ -243,19 +247,174 @@ export class ModuleTestDataService {
243
247
  }
244
248
  }
245
249
 
250
+ // Strip media fields from entity payload — file paths cannot be saved as columns
251
+ const mediaPayload: Record<string, string> = {};
252
+ for (const field of modelDef.fields ?? []) {
253
+ if ((field.type === 'mediaSingle' || field.type === 'mediaMultiple') && payload[field.name] !== undefined) {
254
+ mediaPayload[field.name] = payload[field.name] as string;
255
+ delete payload[field.name];
256
+ }
257
+ }
258
+
259
+ // Strip many-to-many and one-to-many fields — these are resolved post-save via the relation builder
260
+ const multiRelationPayload: Array<{ field: any; userKeys: string[] }> = [];
261
+ for (const field of modelDef.fields ?? []) {
262
+ if (field.type !== 'relation') continue;
263
+ if (field.relationType !== 'many-to-many' && field.relationType !== 'one-to-many') continue;
264
+
265
+ const userKeysProp = `${field.name}UserKeys`;
266
+ if (userKeysProp in payload && Array.isArray(payload[userKeysProp])) {
267
+ multiRelationPayload.push({ field, userKeys: payload[userKeysProp] });
268
+ delete payload[userKeysProp];
269
+ }
270
+ // Remove raw field value if accidentally present
271
+ delete payload[field.name];
272
+ }
273
+
274
+ // Upsert entity, capturing the saved result for post-save steps
275
+ let savedEntity: any;
246
276
  const userKeyField = modelDef.userKeyFieldUserKey;
247
277
  if (userKeyField && payload[userKeyField] !== undefined) {
248
278
  const existing = await entityRepo.findOne({
249
279
  where: { [userKeyField]: payload[userKeyField] },
250
280
  });
251
281
  if (existing) {
252
- await entityRepo.save(entityRepo.merge(existing, payload));
253
- continue;
282
+ savedEntity = await entityRepo.save(entityRepo.merge(existing, payload));
283
+ } else {
284
+ savedEntity = await entityRepo.save(entityRepo.create(payload));
254
285
  }
286
+ } else {
287
+ savedEntity = await entityRepo.save(entityRepo.create(payload));
288
+ }
289
+
290
+ if (multiRelationPayload.length > 0) {
291
+ await this.seedMultiRelations(savedEntity.id, modelUserKey, multiRelationPayload, modelsByName);
292
+ }
293
+
294
+ if (Object.keys(mediaPayload).length > 0) {
295
+ await this.seedEntityMedia(savedEntity.id, modelUserKey, mediaPayload);
296
+ }
297
+ }
298
+ }
299
+
300
+ private async seedMultiRelations(
301
+ entityId: number,
302
+ modelUserKey: string,
303
+ relations: Array<{ field: any; userKeys: string[] }>,
304
+ modelsByName: Map<string, CreateModelMetadataDto>,
305
+ ): Promise<void> {
306
+ for (const { field, userKeys } of relations) {
307
+ if (!userKeys.length) continue;
308
+
309
+ const coModelName = field.relationCoModelSingularName;
310
+ const coModelDef = modelsByName.get(coModelName);
311
+ if (!coModelDef) {
312
+ throw new Error(`Relation model "${coModelName}" not found in metadata for field ${modelUserKey}.${field.name}`);
313
+ }
314
+ const coUserKeyField = coModelDef.userKeyFieldUserKey;
315
+ if (!coUserKeyField) {
316
+ throw new Error(`Relation model "${coModelName}" is missing userKeyFieldUserKey, needed to resolve ${modelUserKey}.${field.name}`);
317
+ }
318
+
319
+ const coRepo = this.resolveRepository(coModelName);
320
+ const resolvedIds: number[] = [];
321
+ for (const uk of userKeys) {
322
+ const related = typeof coRepo.findOneByUserKey === 'function'
323
+ ? await coRepo.findOneByUserKey(uk)
324
+ : await coRepo.findOne({ where: { [coUserKeyField]: uk } });
325
+ if (!related) {
326
+ throw new Error(`Related entity not found: ${coModelName}.${coUserKeyField}=${uk}`);
327
+ }
328
+ resolvedIds.push(related.id);
329
+ }
330
+
331
+ // Load currently associated entities to diff (set semantics — idempotent)
332
+ const existingRelated: any[] = await this.entityManager
333
+ .createQueryBuilder()
334
+ .relation(classify(modelUserKey), field.name)
335
+ .of(entityId)
336
+ .loadMany();
337
+ const existingIds: number[] = existingRelated.map((e) => e.id);
338
+
339
+ const toAdd = resolvedIds.filter((id) => !existingIds.includes(id));
340
+ const toRemove = existingIds.filter((id) => !resolvedIds.includes(id));
341
+
342
+ if (toAdd.length > 0 || toRemove.length > 0) {
343
+ await this.entityManager
344
+ .createQueryBuilder()
345
+ .relation(classify(modelUserKey), field.name)
346
+ .of(entityId)
347
+ .addAndRemove(toAdd, toRemove);
255
348
  }
256
349
 
257
- await entityRepo.save(entityRepo.create(payload));
350
+ this.logger.debug(`Seeded ${field.relationType} relation ${modelUserKey}.${field.name} entityId=${entityId}: +${toAdd.length} -${toRemove.length}`);
351
+ }
352
+ }
353
+
354
+ private async seedEntityMedia(
355
+ entityId: number,
356
+ modelUserKey: string,
357
+ mediaPayload: Record<string, string>,
358
+ ): Promise<void> {
359
+ const mediaBasePath = process.env.TEST_UPLOADS_MEDIA_FILE_PATH;
360
+ if (!mediaBasePath) {
361
+ throw new Error('TEST_UPLOADS_MEDIA_FILE_PATH is not set. Cannot seed test media.');
258
362
  }
363
+
364
+ const modelMetadata = await this.modelMetadataService.findOneBySingularName(modelUserKey, {
365
+ fields: {
366
+ model: { userKeyField: true },
367
+ mediaStorageProvider: true,
368
+ },
369
+ });
370
+
371
+ for (const [fieldName, fileName] of Object.entries(mediaPayload)) {
372
+ if (!fileName) continue;
373
+
374
+ const fieldMetadata = modelMetadata.fields.find((f) => f.name === fieldName);
375
+ if (!fieldMetadata) {
376
+ throw new Error(`Media field "${fieldName}" not found in loaded metadata for model ${modelUserKey}`);
377
+ }
378
+ if (!fieldMetadata.mediaStorageProvider) {
379
+ throw new Error(`Media field "${fieldName}" in model ${modelUserKey} has no storage provider configured`);
380
+ }
381
+
382
+ const storageProviderType = fieldMetadata.mediaStorageProvider.type as MediaStorageProviderType;
383
+ if (storageProviderType !== MediaStorageProviderType.Filesystem) {
384
+ throw new Error(`Test media seeding supports filesystem storage only. Field "${fieldName}" uses "${storageProviderType}".`);
385
+ }
386
+
387
+ // Idempotency: skip if media already exists for this entity + field
388
+ const existing = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(
389
+ entityId, fieldMetadata.id, fieldMetadata.model.id,
390
+ );
391
+ if (existing.length > 0) {
392
+ this.logger.debug(`Media already seeded for ${modelUserKey}.${fieldName} entityId=${entityId}, skipping`);
393
+ continue;
394
+ }
395
+
396
+ const sourcePath = path.join(mediaBasePath, fileName);
397
+ if (!fs.existsSync(sourcePath)) {
398
+ throw new Error(`Test media file not found: ${sourcePath}`);
399
+ }
400
+
401
+ const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType);
402
+ const stream = fs.createReadStream(sourcePath);
403
+ await storageProvider.storeStreams([[stream, fileName]], { id: entityId }, fieldMetadata);
404
+ this.logger.debug(`Seeded media for ${modelUserKey}.${fieldName} entityId=${entityId} file=${fileName}`);
405
+ }
406
+ }
407
+
408
+ private get entityManager(): EntityManager {
409
+ return this.moduleRef.get(EntityManager, { strict: false });
410
+ }
411
+
412
+ private get modelMetadataService(): ModelMetadataService {
413
+ return this.moduleRef.get(ModelMetadataService, { strict: false });
414
+ }
415
+
416
+ private get mediaRepository(): MediaRepository {
417
+ return this.moduleRef.get(MediaRepository, { strict: false });
259
418
  }
260
419
 
261
420
  private resolveRepository(modelUserKey: string): any {
@@ -308,7 +467,7 @@ export class ModuleTestDataService {
308
467
  if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) {
309
468
  return line;
310
469
  }
311
- const [rawKey, ...rest] = line.split('=');
470
+ const [rawKey] = line.split('=');
312
471
  const key = rawKey.trim();
313
472
  if (!key.endsWith('_DATABASE_NAME')) {
314
473
  return line;