@solidxai/core 0.1.8-beta.2 → 0.1.8-beta.4

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 (27) hide show
  1. package/dist/dtos/post-chatter-message.dto.d.ts +1 -0
  2. package/dist/dtos/post-chatter-message.dto.d.ts.map +1 -1
  3. package/dist/dtos/post-chatter-message.dto.js +6 -1
  4. package/dist/dtos/post-chatter-message.dto.js.map +1 -1
  5. package/dist/entities/field-metadata.entity.js +1 -1
  6. package/dist/entities/field-metadata.entity.js.map +1 -1
  7. package/dist/entities/legacy-common.entity.d.ts +9 -9
  8. package/dist/entities/legacy-common.entity.d.ts.map +1 -1
  9. package/dist/entities/legacy-common.entity.js +7 -7
  10. package/dist/entities/legacy-common.entity.js.map +1 -1
  11. package/dist/seeders/module-test-data.service.d.ts +5 -0
  12. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  13. package/dist/seeders/module-test-data.service.js +131 -4
  14. package/dist/seeders/module-test-data.service.js.map +1 -1
  15. package/dist/services/chatter-message.service.d.ts.map +1 -1
  16. package/dist/services/chatter-message.service.js +6 -0
  17. package/dist/services/chatter-message.service.js.map +1 -1
  18. package/dist/services/export-transaction.service.d.ts.map +1 -1
  19. package/dist/services/export-transaction.service.js +0 -23
  20. package/dist/services/export-transaction.service.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/dtos/post-chatter-message.dto.ts +4 -0
  23. package/src/entities/field-metadata.entity.ts +1 -1
  24. package/src/entities/legacy-common.entity.ts +15 -15
  25. package/src/seeders/module-test-data.service.ts +165 -6
  26. package/src/services/chatter-message.service.ts +7 -0
  27. package/src/services/export-transaction.service.ts +0 -26
@@ -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;
@@ -53,6 +53,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
53
53
  chatterMessage.messageBody = postDto.messageBody;
54
54
  chatterMessage.coModelEntityId = postDto.coModelEntityId;
55
55
  chatterMessage.coModelName = postDto.coModelName;
56
+ chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
57
+
58
+ const model = await this.modelMetadataRepo.findOne({
59
+ where: { singularName: lowerFirst(postDto.coModelName) },
60
+ relations: { userKeyField: true }
61
+ });
62
+ chatterMessage.modelDisplayName = model?.displayName ?? null;
56
63
 
57
64
  const activeUser = this.requestContextService.getActiveUser();
58
65
 
@@ -274,32 +274,6 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
274
274
  }
275
275
  }
276
276
 
277
- // Include userKey from each related field
278
- for (const [relatedFieldName, userKeyFieldName] of relatedModelsUserKeyMap.entries()) {
279
- const relatedData = record[relatedFieldName];
280
- const displayKey = fieldNameToDisplayName.get(relatedFieldName) ?? relatedFieldName;
281
-
282
- if (Array.isArray(relatedData)) {
283
- // For many-to-many or one-to-many
284
- const values = relatedData
285
- .map(item => {
286
- let val = item?.[userKeyFieldName];
287
- const relatedFieldMeta = modelFields.find(f => f.name === relatedFieldName);
288
- if ((relatedFieldMeta?.type === 'datetime' || relatedFieldMeta?.type === 'date') && val) {
289
- val = new Date(val).toISOString();
290
- }
291
- return val;
292
- })
293
- .filter(Boolean);
294
- newRecord[displayKey] = values.join(', ');
295
- } else if (relatedData && typeof relatedData === 'object') {
296
- // For many-to-one or one-to-one
297
- newRecord[relatedFieldName] = relatedData?.[userKeyFieldName] ?? null;
298
- } else {
299
- newRecord[displayKey] = null;
300
- }
301
- }
302
-
303
277
  // Include userKey from each related field (with displayName)
304
278
  for (const [relatedFieldName, userKeyFieldName] of relatedModelsUserKeyMap.entries()) {
305
279
  const relatedData = record[relatedFieldName];