@solidstarters/solid-core 1.2.161 → 1.2.163

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 (58) hide show
  1. package/dist/commands/ingest.command.d.ts +16 -0
  2. package/dist/commands/ingest.command.d.ts.map +1 -0
  3. package/dist/commands/ingest.command.js +50 -0
  4. package/dist/commands/ingest.command.js.map +1 -0
  5. package/dist/commands/refresh-module.command.d.ts.map +1 -1
  6. package/dist/commands/refresh-module.command.js.map +1 -1
  7. package/dist/controllers/service.controller.d.ts +16 -1
  8. package/dist/controllers/service.controller.d.ts.map +1 -1
  9. package/dist/controllers/service.controller.js +55 -2
  10. package/dist/controllers/service.controller.js.map +1 -1
  11. package/dist/controllers/test.controller.d.ts +6 -1
  12. package/dist/controllers/test.controller.d.ts.map +1 -1
  13. package/dist/controllers/test.controller.js +21 -3
  14. package/dist/controllers/test.controller.js.map +1 -1
  15. package/dist/entities/common.entity.d.ts.map +1 -1
  16. package/dist/entities/common.entity.js +14 -2
  17. package/dist/entities/common.entity.js.map +1 -1
  18. package/dist/entities/user.entity.d.ts.map +1 -1
  19. package/dist/entities/user.entity.js +11 -1
  20. package/dist/entities/user.entity.js.map +1 -1
  21. package/dist/helpers/error-mapper.service.d.ts +8 -0
  22. package/dist/helpers/error-mapper.service.d.ts.map +1 -0
  23. package/dist/helpers/error-mapper.service.js +108 -0
  24. package/dist/helpers/error-mapper.service.js.map +1 -0
  25. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.d.ts.map +1 -1
  26. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.js +4 -2
  27. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.js.map +1 -1
  28. package/dist/seeders/seed-data/solid-core-metadata.json +21 -0
  29. package/dist/services/genai/ingest-metadata.service.d.ts +38 -0
  30. package/dist/services/genai/ingest-metadata.service.d.ts.map +1 -0
  31. package/dist/services/genai/ingest-metadata.service.js +530 -0
  32. package/dist/services/genai/ingest-metadata.service.js.map +1 -0
  33. package/dist/services/genai/r2r-helper.service.d.ts +7 -0
  34. package/dist/services/genai/r2r-helper.service.d.ts.map +1 -0
  35. package/dist/services/genai/r2r-helper.service.js +36 -0
  36. package/dist/services/genai/r2r-helper.service.js.map +1 -0
  37. package/dist/services/setting.service.d.ts.map +1 -1
  38. package/dist/services/setting.service.js +38 -20
  39. package/dist/services/setting.service.js.map +1 -1
  40. package/dist/solid-core.module.d.ts.map +1 -1
  41. package/dist/solid-core.module.js +8 -0
  42. package/dist/solid-core.module.js.map +1 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +2 -1
  45. package/src/commands/ingest-rag-chunking-strategy-for.md +224 -0
  46. package/src/commands/ingest.command.ts +36 -0
  47. package/src/commands/refresh-module.command.ts +0 -1
  48. package/src/controllers/service.controller.ts +66 -3
  49. package/src/controllers/test.controller.ts +15 -3
  50. package/src/entities/common.entity.ts +10 -0
  51. package/src/entities/user.entity.ts +33 -1
  52. package/src/helpers/error-mapper.service.ts +214 -0
  53. package/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts +4 -2
  54. package/src/seeders/seed-data/solid-core-metadata.json +21 -0
  55. package/src/services/genai/ingest-metadata.service.ts +695 -0
  56. package/src/services/genai/r2r-helper.service.ts +33 -0
  57. package/src/services/setting.service.ts +46 -22
  58. package/src/solid-core.module.ts +8 -0
@@ -0,0 +1,695 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import * as crypto from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ import { getDynamicModuleNames } from 'src/helpers/module.helper';
7
+ import { CreateModuleMetadataDto } from 'src/dtos/create-module-metadata.dto';
8
+ import { R2RHelperService } from './r2r-helper.service';
9
+ import { CollectionResponse, r2rClient } from 'r2r-js';
10
+ import { CreateModelMetadataDto } from 'src/dtos/create-model-metadata.dto';
11
+ import { CreateFieldMetadataDto } from 'src/dtos/create-field-metadata.dto';
12
+
13
+ export type FieldIngestionInfo = {
14
+ fieldName: string;
15
+ fieldChunkId?: string;
16
+ fieldHash?: string;
17
+ };
18
+
19
+ export type ModelIngestionInfo = {
20
+ modelName: string;
21
+ modelChunkId?: string;
22
+ modelHash?: string;
23
+ fields: FieldIngestionInfo[];
24
+ };
25
+
26
+ export type ModuleRAGIngestionInfo = {
27
+ moduleName?: string;
28
+ collectionId?: string;
29
+
30
+ // Full json document is also uploaded so we track references...
31
+ documentId?: string;
32
+ documentHash?: string;
33
+
34
+ // module references
35
+ moduleChunkId?: string;
36
+ // track a hash of module metadata to skip unchanged
37
+ moduleHash?: string;
38
+
39
+ // model references...
40
+ models: ModelIngestionInfo[];
41
+ };
42
+
43
+ @Injectable()
44
+ export class IngestMetadataService {
45
+ private readonly logger = new Logger(IngestMetadataService.name);
46
+ private ragClient: r2rClient;
47
+
48
+ constructor(
49
+ private readonly r2rService: R2RHelperService,
50
+ ) { }
51
+
52
+ // Stable stringify so hashes/ids don't flap
53
+ // private stableStringify(obj: any): string {
54
+ // return JSON.stringify(obj, Object.keys(obj).sort(), 2);
55
+ // }
56
+ // private sha256(input: string): string {
57
+ // return crypto.createHash('sha256').update(input).digest('hex');
58
+ // }
59
+ // private hashSchema(obj: any): string {
60
+ // return this.sha256(this.stableStringify(obj));
61
+ // }
62
+ private _sha256OfJson(obj: any): string {
63
+ const s = JSON.stringify(obj);
64
+ return crypto.createHash('sha256').update(s).digest('hex');
65
+ }
66
+ // // Small natural-language one-liners for relations
67
+ // private relationSig(model: any): string[] {
68
+ // const rels: string[] = [];
69
+ // for (const f of model.fields ?? []) {
70
+ // if (f.relation?.targetModel) {
71
+ // rels.push(`${model.singularName}.${f.name} -> ${f.relation.targetModel}(${f.relation.targetField ?? 'id'})`);
72
+ // }
73
+ // }
74
+ // return rels;
75
+ // }
76
+
77
+ private _oneLineBool(b?: boolean): 'yes' | 'no' {
78
+ return b ? 'yes' : 'no';
79
+ }
80
+
81
+ private _shortList(arr?: string[] | null, max = 10): string {
82
+ if (!Array.isArray(arr) || arr.length === 0) return 'none';
83
+ const a = arr.slice(0, max);
84
+ return `${a.join(', ')}${arr.length > max ? ', …' : ''}`;
85
+ }
86
+
87
+ async ingest() {
88
+ // Create a new ragClient...
89
+ this.ragClient = await this.r2rService.getClient();
90
+
91
+ // const allModuleMetadataJson = [];
92
+ this.logger.debug(`getting dynamics modules`);
93
+ const enabledModules = getDynamicModuleNames();
94
+ this.logger.log(`ingesting metadata`);
95
+
96
+ for (let i = 0; i < enabledModules.length; i++) {
97
+ const enabledModule = enabledModules[i];
98
+ const fileName = `${enabledModule}-metadata.json`;
99
+ const enabledModuleSeedFile = `module-metadata/${enabledModule}/${fileName}`;
100
+ const fullPath = path.join(process.cwd(), enabledModuleSeedFile);
101
+ const overallMetadata: any = JSON.parse(fs.readFileSync(fullPath, 'utf-8').toString());
102
+
103
+ const moduleMetadata: CreateModuleMetadataDto = overallMetadata.moduleMetadata;
104
+
105
+ // Manage all the ingestion info file paths...
106
+ const enabledModulIngestionInfoFile = `module-metadata/${enabledModule}/genai/${enabledModule}-ingested-info.json`;
107
+ const enabledModulIngestionInfoFullPath = path.join(process.cwd(), enabledModulIngestionInfoFile);
108
+ const ingestionInfo: ModuleRAGIngestionInfo = fs.existsSync(enabledModulIngestionInfoFullPath) ? JSON.parse(fs.readFileSync(enabledModulIngestionInfoFullPath, 'utf-8').toString()) : {};
109
+ const enabledModulIngestionInfoDir = path.dirname(enabledModulIngestionInfoFullPath);
110
+ if (!fs.existsSync(enabledModulIngestionInfoDir)) {
111
+ fs.mkdirSync(enabledModulIngestionInfoDir, { recursive: true });
112
+ }
113
+
114
+ ingestionInfo.moduleName = enabledModule
115
+
116
+ // Process module metadata first.
117
+ this.logger.log(`[Start] Processing module metadata for ${moduleMetadata.name}`)
118
+
119
+ // Create or use an existing collection...
120
+ const collectionId = await this.resolveRagCollectionForModule(ingestionInfo, enabledModule)
121
+ ingestionInfo.collectionId = collectionId;
122
+
123
+ // Delete and re-insert a document representing the full json...
124
+ await this.deleteInsertRagDocumentForModuleMetadataJsonFile(ingestionInfo, fullPath, fileName)
125
+
126
+ // Delete and re-insert a chunk representing the module.
127
+ await this.deleteInsertRagChunkForModule(ingestionInfo, moduleMetadata);
128
+
129
+ // Delete and re-insert chunks representing each model.
130
+ for (const model of moduleMetadata.models) {
131
+ await this.deleteInsertRagChunkForModel(ingestionInfo, enabledModule, model);
132
+ for (const field of model.fields) {
133
+ await this.deleteInsertRagChunkForField(ingestionInfo, enabledModule, model.singularName, field);
134
+ }
135
+ }
136
+
137
+ // Save ingestion info to disk...
138
+ fs.writeFileSync(enabledModulIngestionInfoFullPath, JSON.stringify({ ...ingestionInfo }, null, 2), 'utf8');
139
+ }
140
+ }
141
+
142
+ private async resolveRagCollectionForModule(ingestionInfo: ModuleRAGIngestionInfo, moduleName: string): Promise<string> {
143
+ this.logger.debug(`Resolving RAG collection for module: ${moduleName}`);
144
+
145
+ let existingCollection: CollectionResponse = null;
146
+ if (ingestionInfo.collectionId) {
147
+ // See if collection already exists...
148
+ const r = await this.ragClient.collections.list({
149
+ ids: [
150
+ ingestionInfo.collectionId
151
+ ]
152
+ });
153
+
154
+ if (r) {
155
+ if (r.results.length === 1) {
156
+ existingCollection = r.results[0];
157
+ }
158
+ if (r.results.length > 1) {
159
+ // TODO: do something that will print a meaningful error on the console...
160
+ }
161
+ }
162
+ }
163
+
164
+ if (!existingCollection) {
165
+ const r = await this.ragClient.collections.create({
166
+ name: `${moduleName}-rag-collection`,
167
+ description: `Collection created to group all documents, chunks related to module: ${moduleName}`
168
+ });
169
+
170
+ // TODO: for some reason if we are unable to create a collection then fail with a visible error message in the console...
171
+
172
+ return r.results.id;
173
+ }
174
+
175
+ return existingCollection.id;
176
+ }
177
+
178
+ private async deleteInsertRagDocumentForModuleMetadataJsonFile(ingestionInfo: ModuleRAGIngestionInfo, fullPath: string, fileName: string): Promise<void> {
179
+ this.logger.debug(`Ingesting file: ${fullPath}`);
180
+
181
+ // 1) Compute hash of the entire JSON string (as-is)
182
+ const jsonStr = fs.readFileSync(fullPath, 'utf-8');
183
+ const contentHash = this._sha256OfJson(JSON.parse(jsonStr));
184
+
185
+ // 2) Short-circuit if unchanged and we still have a documentId
186
+ if (ingestionInfo.documentHash === contentHash && ingestionInfo.documentId) {
187
+ this.logger.log(`[Skip] Unchanged: ${fileName} (hash=${contentHash.slice(0, 8)}…)`);
188
+ return;
189
+ // return ingestionInfo.documentId;
190
+ }
191
+
192
+ // 3) Delete the previous doc if present
193
+ if (ingestionInfo.documentId) {
194
+ try {
195
+ await this.ragClient.documents.delete({ id: ingestionInfo.documentId });
196
+ } catch (e) {
197
+ this.logger.warn(
198
+ `[Warn] Failed deleting prior document ${ingestionInfo.documentId}: ${String(e)}`
199
+ );
200
+ }
201
+ }
202
+
203
+ // 4) Create a fresh document; attach the hash into metadata for traceability
204
+ const ingestResult = await this.ragClient.documents.create({
205
+ file: {
206
+ path: fullPath,
207
+ name: fileName
208
+ },
209
+ collectionIds: [ingestionInfo.collectionId],
210
+ metadata: {
211
+ contentHash,
212
+ fileName
213
+ },
214
+ });
215
+ // console.log("file ingest result:", JSON.stringify(ingestResult, null, 2));
216
+
217
+ const newId = ingestResult?.results?.documentId;
218
+ if (!newId) {
219
+ throw new Error(`R2R did not return a documentId for ${fileName}`);
220
+ }
221
+
222
+ // 5) Persist identifiers + hash on our side
223
+ ingestionInfo.documentId = newId;
224
+ ingestionInfo.documentHash = contentHash;
225
+
226
+ this.logger.log(`[OK] Ingested ${fileName} → id=${newId}, hash=${contentHash.slice(0, 8)}…`);
227
+ // return newId;
228
+
229
+ }
230
+
231
+ private async deleteInsertRagChunkForModule(ingestionInfo: ModuleRAGIngestionInfo, moduleMetadata: CreateModuleMetadataDto): Promise<void> {
232
+ const moduleName: string = moduleMetadata?.name;
233
+
234
+ // Hash the meaningful parts of the module to detect changes and skip re-ingest.
235
+ const schemaHash = this._sha256OfJson({
236
+ name: moduleMetadata?.name ?? null,
237
+ description: moduleMetadata?.description ?? null,
238
+
239
+ // Keep model names + brief shape so module-level hash changes when models change.
240
+ models: (moduleMetadata?.models ?? []).map((m: any) => ({
241
+ singularName: m?.singularName ?? null,
242
+ description: m?.description ?? null,
243
+
244
+ // Include field names to detect field-level changes at module granularity - maybe remove this later?
245
+ fields: Array.isArray(m?.fields) ? m.fields.map((f: any) => f?.name ?? null) : [],
246
+ })),
247
+ });
248
+
249
+ // Skip unchanged module
250
+ if (ingestionInfo.moduleHash === schemaHash && ingestionInfo.moduleChunkId) {
251
+ this.logger.log(`[Skip] Module unchanged: ${moduleName}`);
252
+ return;
253
+ // return ingestionInfo.moduleChunkId;
254
+ }
255
+
256
+ const models: any[] = moduleMetadata?.models ?? [];
257
+ const modelLines = models.map((m) => {
258
+ const name = m?.singularName;
259
+ const desc = m?.description ? `: ${m.description}` : '';
260
+ return `- ${name}${desc}`;
261
+ });
262
+
263
+ const text = `SolidX Module: ${moduleName}
264
+ Purpose: ${moduleMetadata?.description ?? 'N/A'}
265
+
266
+ Models (${models.length}):
267
+ ${modelLines.join('\n')}
268
+
269
+ Usage: Use this chunk to choose the correct model/field chunks for code generation or metadata edits.`;
270
+
271
+ // metadata has to be concise and queryable
272
+ const metadata = {
273
+ kind: 'solidx-metadata',
274
+ type: 'module',
275
+ moduleName,
276
+ modelCount: models.length,
277
+ schemaHash,
278
+ models: models.map((m) => m?.singularName).filter(Boolean),
279
+ };
280
+
281
+ // Delete previous chunk if we have one
282
+ if (ingestionInfo.moduleChunkId) {
283
+ try {
284
+ await this.ragClient.documents.delete({ id: ingestionInfo.moduleChunkId });
285
+ } catch (e) {
286
+ this.logger.warn(`[Warn] Failed deleting old module chunk (${ingestionInfo.moduleChunkId}): ${String(e)}`);
287
+ }
288
+ }
289
+
290
+ const r = await this.ragClient.documents.create({
291
+ raw_text: text,
292
+ metadata: metadata,
293
+ collectionIds: [ingestionInfo.collectionId],
294
+ });
295
+
296
+ const newId = r?.results?.documentId;
297
+ if (!newId) {
298
+ throw new Error(`R2R did not return a documentId while creating module chunk for module name ${moduleName}`);
299
+ }
300
+
301
+ // Update ingestion info for persistence by the caller
302
+ ingestionInfo.moduleChunkId = r.results?.documentId;
303
+ ingestionInfo.moduleHash = schemaHash;
304
+
305
+ this.logger.log(`[OK] Ingested module ${moduleName} → id=${newId}, hash=${schemaHash.slice(0, 8)}…`);
306
+
307
+ }
308
+
309
+ private async deleteInsertRagChunkForModel(ingestionInfo: ModuleRAGIngestionInfo, moduleName: string, model: CreateModelMetadataDto): Promise<void> {
310
+ const modelName: string = model?.singularName;
311
+
312
+ // 1) Hash full JSON (as-is) to detect changes and skip re-ingest
313
+ const schemaHash = this._sha256OfJson(model);
314
+
315
+ // Ensure ingestionInfo.models[] has an entry for this model
316
+ const modelsArr = ingestionInfo.models ?? (ingestionInfo.models = []);
317
+ let modelEntry = modelsArr.find(m => m.modelName === modelName);
318
+ if (!modelEntry) {
319
+ modelEntry = { modelName, fields: [] };
320
+ modelsArr.push(modelEntry);
321
+ }
322
+
323
+ // 2) Short-circuit if unchanged
324
+ if (modelEntry.modelHash === schemaHash && modelEntry.modelChunkId) {
325
+ this.logger.log(`[Skip] Model unchanged: ${moduleName}.${modelName}`);
326
+ return;
327
+ // return modelEntry.modelChunkId;
328
+ }
329
+
330
+ // 3) Build retrieval-friendly text (concise)
331
+ const fields: CreateFieldMetadataDto[] = Array.isArray(model?.fields) ? model.fields : [];
332
+ const userkey = fields.find((f: CreateFieldMetadataDto) => f?.isUserKey)?.name ?? 'id';
333
+ const uniques = fields.filter((f: CreateFieldMetadataDto) => f?.unique).map((f: any) => f.name);
334
+ const required = fields.filter((f: CreateFieldMetadataDto) => f?.required).map((f: any) => f.name);
335
+ const rels = fields
336
+ .filter((f: CreateFieldMetadataDto) => f.type === 'relation')
337
+ .map((f: CreateFieldMetadataDto) => `${modelName}.${f.name} -> ${f.relationCoModelSingularName}.${f.relationCoModelColumnName ?? 'id'}`);
338
+
339
+ const fieldSummaryLines = fields.slice(0, 30).map((f: CreateFieldMetadataDto) => {
340
+ const bits = [
341
+ `${f.name}:${f.type}`,
342
+ f.required ? 'req' : '',
343
+ f.unique ? 'unique' : '',
344
+ f.isUserKey ? 'userkey' : '',
345
+ f.relationCoModelSingularName ? `rel->${f.relationCoModelSingularName}` : '',
346
+ ].filter(Boolean).join('|');
347
+ return `- ${bits}`;
348
+ });
349
+
350
+ const text =
351
+ `SolidX Model: ${modelName}
352
+ Module: ${moduleName}
353
+ Purpose: ${model?.description ?? 'N/A'}
354
+
355
+ Signature:
356
+ - Primary: ${userkey}
357
+ - Unique: ${uniques.length ? uniques.join(', ') : 'none'}
358
+ - Required (${required.length}): ${required.slice(0, 12).join(', ')}${required.length > 12 ? '…' : ''}
359
+
360
+ Relations (${rels.length}):
361
+ ${rels.length ? `- ${rels.join('\n- ')}` : 'None'}
362
+
363
+ Fields (${fields.length}) [name:type|flags]:
364
+ ${fieldSummaryLines.join('\n')}
365
+
366
+ Usage: Use this chunk to generate DTOs, subscribers, custom service methods, and CRUD handlers for ${modelName}.
367
+ For exact constraints (enum/min/max/regex/default), consult the individual field chunks.`;
368
+
369
+ // 4) Metadata (concise & queryable)
370
+ const metadata = {
371
+ kind: 'solidx-metadata',
372
+ type: 'model',
373
+ moduleName,
374
+ modelName,
375
+ fieldCount: fields.length,
376
+ requiredCount: required.length,
377
+ relationCount: rels.length,
378
+ userkey: userkey,
379
+ uniqueFields: uniques,
380
+ hasTimestamps: !!fields.find((f: CreateFieldMetadataDto) => ['time', 'date', 'datetime'].includes(f.type)),
381
+ schemaHash,
382
+ };
383
+
384
+ // 5) Delete previous chunk if present
385
+ if (modelEntry.modelChunkId) {
386
+ try {
387
+ await this.ragClient.documents.delete({ id: modelEntry.modelChunkId });
388
+ } catch (e) {
389
+ this.logger.warn(`[Warn] Failed deleting old model chunk (${modelEntry.modelChunkId}): ${String(e)}`);
390
+ }
391
+ }
392
+
393
+ // 6) Create new document (R2R auto-generates the ID)
394
+ const r = await this.ragClient.documents.create({
395
+ raw_text: text,
396
+ metadata,
397
+ collectionIds: [ingestionInfo.collectionId],
398
+ });
399
+
400
+ const newId = r?.results?.documentId;
401
+ if (!newId) {
402
+ throw new Error(`R2R did not return a documentId while creating model chunk for ${moduleName}.${modelName}`);
403
+ }
404
+
405
+ // 7) Update ingestionInfo for persistence
406
+ modelEntry.modelChunkId = newId;
407
+ modelEntry.modelHash = schemaHash;
408
+
409
+ this.logger.log(`[OK] Ingested model ${moduleName}.${modelName} → id=${newId}, hash=${schemaHash.slice(0, 8)}…`);
410
+ // return newId;
411
+ }
412
+
413
+ private _buildFieldTextAndMetadata(moduleName: string, modelName: string, f: CreateFieldMetadataDto) {
414
+ // Identity
415
+ const name = f?.name;
416
+ const displayName = f?.displayName ?? name;
417
+ const description = f?.description ?? 'N/A';
418
+
419
+ // Types
420
+ const type = f?.type;
421
+ const ormType = f?.ormType ?? null;
422
+
423
+ // Constraints / validation
424
+ const required = !!f?.required;
425
+ const unique = !!f?.unique;
426
+ const index = !!f?.index;
427
+ const length = f?.length ?? null;
428
+ const min = f?.min ?? null;
429
+ const max = f?.max ?? null;
430
+ const defaultValue = f?.defaultValue ?? null;
431
+ const regex = f?.regexPattern ?? null;
432
+ const regexErr = f?.regexPatternNotMatchingErrorMsg ?? null;
433
+
434
+ // Relation
435
+ const relationType = f?.relationType ?? null; // e.g., many-to-one, many-to-many, one-to-many
436
+ const relModule = f?.relationModelModuleName ?? null;
437
+ const relModel = f?.relationCoModelSingularName ?? null;
438
+ const relField = f?.relationCoModelFieldName ?? 'id';
439
+ // const relOwner = f?.isRelationManyToManyOwner ?? null;
440
+ // const relJoinTable = f?.relationJoinTableName ?? null;
441
+ // const relCreateInverse = !!f?.relationCreateInverse;
442
+ const relCascade = f?.relationCascade ?? null;
443
+ const relFixedFilter = f?.relationFieldFixedFilter ?? null;
444
+
445
+ // Selection (dropdowns)
446
+ const selectionDynProvider = f?.selectionDynamicProvider ?? null;
447
+ const selectionDynCtxt = f?.selectionDynamicProviderCtxt ?? null;
448
+ const selectionStatic = Array.isArray(f?.selectionStaticValues) ? f.selectionStaticValues : null;
449
+ const selectionValueType = f?.selectionValueType ?? null;
450
+ const isMultiSelect = !!f?.isMultiSelect;
451
+
452
+ // Media (uploads)
453
+ const mediaTypes = Array.isArray(f?.mediaTypes) ? f.mediaTypes : null;
454
+ const mediaMaxSizeKb = f?.mediaMaxSizeKb ?? null;
455
+ const mediaStorageProvider = f?.mediaStorageProviderUserKey ?? null; // likely object/id in your JSON
456
+
457
+ // Computed fields
458
+ const computedProvider = f?.computedFieldValueProvider ?? null;
459
+ const computedProviderCtxt = f?.computedFieldValueProviderCtxt ?? null;
460
+ const computedValueType = f?.computedFieldValueType ?? null;
461
+ const computedTriggerCfg = f?.computedFieldTriggerConfig ?? null;
462
+
463
+ // Security / privacy / audit
464
+ // const encrypt = !!f?.encrypt;
465
+ // const encryptionType = f?.encryptionType ?? null;
466
+ // const decryptWhen = f?.decryptWhen ?? null;
467
+ const isPrivate = !!f?.private;
468
+ const enableAuditTracking = !!f?.enableAuditTracking;
469
+
470
+ // Keys / system flags / mapping
471
+ const isUserKey = !!f?.isUserKey;
472
+ const isSystem = !!f?.isSystem;
473
+ const isMarkedForRemoval = !!f?.isMarkedForRemoval;
474
+ const columnName = f?.columnName ?? null;
475
+ const relCoModelColumn = f?.relationCoModelColumnName ?? null;
476
+ const uuid = f?.uuid ?? null;
477
+
478
+ const relationSummary = (() => {
479
+ if (!relationType || !relModel) return 'none';
480
+ const parts = [
481
+ `type=${relationType}`,
482
+ relModule ? `module=${relModule}` : null,
483
+ `model=${relModel}`,
484
+ relField ? `field=${relField}` : null,
485
+ // relOwner !== null ? `m2mOwner=${relOwner}` : null,
486
+ // relCreateInverse ? 'createInverse=yes' : null,
487
+ relCascade ? `cascade=${relCascade}` : null,
488
+ // relJoinTable ? `joinTable=${relJoinTable}` : null,
489
+ relFixedFilter ? `fixedFilter=${relFixedFilter}` : null,
490
+ ].filter(Boolean);
491
+ return parts.join(', ');
492
+ })();
493
+
494
+ const selectionSummary = (() => {
495
+ const parts: string[] = [];
496
+ if (selectionDynProvider) parts.push(`dynamicProvider=${selectionDynProvider}`);
497
+ if (selectionDynCtxt) parts.push(`dynamicCtxt=${selectionDynCtxt}`);
498
+ if (selectionStatic?.length) parts.push(`static=[${this._shortList(selectionStatic, 12)}]`);
499
+ if (selectionValueType) parts.push(`valueType=${selectionValueType}`);
500
+ parts.push(`multiSelect=${this._oneLineBool(isMultiSelect)}`);
501
+ return parts.length ? parts.join(', ') : 'none';
502
+ })();
503
+
504
+ const mediaSummary = (() => {
505
+ const parts: string[] = [];
506
+ if (mediaTypes?.length) parts.push(`types=[${this._shortList(mediaTypes, 12)}]`);
507
+ if (mediaMaxSizeKb) parts.push(`maxSizeKb=${mediaMaxSizeKb}`);
508
+ if (mediaStorageProvider) parts.push(`storageProvider=${typeof mediaStorageProvider === 'string' ? mediaStorageProvider : 'set'}`);
509
+ return parts.length ? parts.join(', ') : 'none';
510
+ })();
511
+
512
+ const computedSummary = (() => {
513
+ const parts: string[] = [];
514
+ if (computedProvider) parts.push(`provider=${computedProvider}`);
515
+ if (computedProviderCtxt) parts.push(`providerCtxt=${computedProviderCtxt}`);
516
+ if (computedValueType) parts.push(`valueType=${computedValueType}`);
517
+ if (computedTriggerCfg?.length) parts.push(`triggers=${computedTriggerCfg.length}`);
518
+ return parts.length ? parts.join(', ') : 'none';
519
+ })();
520
+
521
+ const securitySummary = [
522
+ // `encrypt=${this.oneLineBool(encrypt)}`,
523
+ // encryptionType ? `encType=${encryptionType}` : null,
524
+ // decryptWhen ? `decryptWhen=${decryptWhen}` : null,
525
+ `private=${this._oneLineBool(isPrivate)}`,
526
+ `auditTracking=${this._oneLineBool(enableAuditTracking)}`
527
+ ].filter(Boolean).join(', ');
528
+
529
+ const constraintSummary = [
530
+ `required=${this._oneLineBool(required)}`,
531
+ `unique=${this._oneLineBool(unique)}`,
532
+ `index=${this._oneLineBool(index)}`,
533
+ length !== null ? `length=${length}` : null,
534
+ min !== null ? `min=${min}` : null,
535
+ max !== null ? `max=${max}` : null,
536
+ defaultValue !== null ? `default=${defaultValue}` : null,
537
+ regex ? `regex=${regex}${regexErr ? ` (${regexErr})` : ''}` : null
538
+ ].filter(Boolean).join(', ');
539
+
540
+ const mappingSummary = [
541
+ columnName ? `column=${columnName}` : null,
542
+ relCoModelColumn ? `relColumn=${relCoModelColumn}` : null,
543
+ uuid ? `uuid=${uuid}` : null,
544
+ `userKey=${this._oneLineBool(isUserKey)}`,
545
+ `system=${this._oneLineBool(isSystem)}`,
546
+ `markedForRemoval=${this._oneLineBool(isMarkedForRemoval)}`
547
+ ].filter(Boolean).join(', ');
548
+
549
+ const text = [
550
+ `SolidX Field: ${name} (${displayName})`,
551
+ `Model: ${modelName}`,
552
+ `Module: ${moduleName}`,
553
+ ``,
554
+ `Type: ${type}${ormType ? ` (orm=${ormType})` : ''}`,
555
+ `Description: ${description}`,
556
+ ``,
557
+ `Constraints: ${constraintSummary || 'none'}`,
558
+ `Relation: ${relationSummary}`,
559
+ `Selection: ${selectionSummary}`,
560
+ `Media: ${mediaSummary}`,
561
+ `Computed: ${computedSummary}`,
562
+ `Security/Privacy/Audit: ${securitySummary}`,
563
+ `Mapping/Flags: ${mappingSummary || 'none'}`,
564
+ ``,
565
+ `Usage: Use this chunk to generate exact field contracts (DTO, form control, DB column), `,
566
+ `validation rules, relation wiring, and UI widgets (selection/media/computed).`,
567
+ ].join('\n');
568
+
569
+ const metadata = {
570
+ kind: 'solidx-metadata',
571
+ type: 'field',
572
+ moduleName,
573
+ modelName,
574
+ fieldName: name,
575
+ displayName,
576
+ description,
577
+ dataType: type,
578
+ ormType,
579
+ required,
580
+ unique,
581
+ index,
582
+ defaultValue,
583
+ length,
584
+ min,
585
+ max,
586
+ regexPattern: regex,
587
+ regexPatternNotMatchingErrorMsg: regexErr,
588
+
589
+ // relation
590
+ relationType,
591
+ relationModelModuleName: relModule,
592
+ relationCoModelSingularName: relModel,
593
+ relationCoModelFieldName: relField,
594
+ // isRelationManyToManyOwner: relOwner,
595
+ // relationJoinTableName: relJoinTable,
596
+ // relationCreateInverse: relCreateInverse,
597
+ relationCascade: relCascade,
598
+ relationFieldFixedFilter: relFixedFilter,
599
+
600
+ // selection
601
+ selectionDynProvider,
602
+ selectionDynCtxt,
603
+ selectionStaticValues: selectionStatic,
604
+ selectionValueType,
605
+ isMultiSelect,
606
+
607
+ // media
608
+ mediaTypes,
609
+ mediaMaxSizeKb,
610
+ mediaStorageProvider: mediaStorageProvider ? (typeof mediaStorageProvider === 'string' ? mediaStorageProvider : 'set') : null,
611
+
612
+ // computed
613
+ computedFieldValueProvider: computedProvider,
614
+ computedFieldValueProviderCtxt: computedProviderCtxt,
615
+ computedFieldValueType: computedValueType,
616
+ computedFieldTriggerConfigCount: Array.isArray(computedTriggerCfg) ? computedTriggerCfg.length : 0,
617
+
618
+ // security/privacy/audit
619
+ // encrypt,
620
+ // encryptionType,
621
+ // decryptWhen,
622
+ private: isPrivate,
623
+ enableAuditTracking,
624
+
625
+ // mapping/flags
626
+ columnName,
627
+ relationCoModelColumnName: relCoModelColumn,
628
+ isUserKey,
629
+ isSystem,
630
+ isMarkedForRemoval,
631
+ };
632
+
633
+ return { text, metadata };
634
+ }
635
+
636
+ private async deleteInsertRagChunkForField(ingestionInfo: ModuleRAGIngestionInfo, moduleName: string, modelName: string, field: CreateFieldMetadataDto,): Promise<string> {
637
+ const fieldName: string = field?.name ?? 'unknown_field';
638
+
639
+ // 1) Full JSON hash (as-is)
640
+ const schemaHash = this._sha256OfJson(field);
641
+
642
+ // 2) Ensure ingestionInfo entry for the model & field
643
+ const modelsArr = ingestionInfo.models ?? (ingestionInfo.models = []);
644
+ let modelEntry = modelsArr.find(m => m.modelName === modelName);
645
+ if (!modelEntry) {
646
+ modelEntry = { modelName, fields: [] };
647
+ modelsArr.push(modelEntry);
648
+ }
649
+ const fieldsArr = modelEntry.fields ?? (modelEntry.fields = []);
650
+ let fieldEntry = fieldsArr.find(f => f.fieldName === fieldName);
651
+ if (!fieldEntry) {
652
+ fieldEntry = { fieldName };
653
+ fieldsArr.push(fieldEntry);
654
+ }
655
+
656
+ // 3) Skip if unchanged
657
+ if (fieldEntry.fieldHash === schemaHash && fieldEntry.fieldChunkId) {
658
+ this.logger.log(`[Skip] Field unchanged: ${moduleName}.${modelName}.${fieldName}`);
659
+ return fieldEntry.fieldChunkId;
660
+ }
661
+
662
+ // 4) Build text + metadata tailored to FieldMetadata
663
+ const { text, metadata } = this._buildFieldTextAndMetadata(moduleName, modelName, field);
664
+ // also keep the hash in metadata for audit/debug
665
+ (metadata as any).schemaHash = schemaHash;
666
+
667
+ // 5) Delete previous chunk if present
668
+ if (fieldEntry.fieldChunkId) {
669
+ try {
670
+ await this.ragClient.documents.delete({ id: fieldEntry.fieldChunkId });
671
+ } catch (e) {
672
+ this.logger.warn(`[Warn] Failed deleting old field chunk (${fieldEntry.fieldChunkId}): ${String(e)}`);
673
+ }
674
+ }
675
+
676
+ // 6) Create new document (R2R auto-generates the ID)
677
+ const r = await this.ragClient.documents.create({
678
+ raw_text: text,
679
+ metadata,
680
+ collectionIds: [ingestionInfo.collectionId],
681
+ });
682
+
683
+ const newId = r?.results?.documentId;
684
+ if (!newId) {
685
+ throw new Error(`R2R did not return a documentId while creating field chunk for ${moduleName}.${modelName}.${fieldName}`);
686
+ }
687
+
688
+ // 7) Update ingestion info
689
+ fieldEntry.fieldChunkId = newId;
690
+ fieldEntry.fieldHash = schemaHash;
691
+
692
+ this.logger.log(`[OK] Ingested field ${moduleName}.${modelName}.${fieldName} → id=${newId}, hash=${schemaHash.slice(0, 8)}…`);
693
+ return newId;
694
+ }
695
+ }