@trokky/client 2.0.3

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 (86) hide show
  1. package/README.md +833 -0
  2. package/dist/cache/manager.d.ts +47 -0
  3. package/dist/cache/manager.d.ts.map +1 -0
  4. package/dist/cache/manager.js +100 -0
  5. package/dist/cache/manager.js.map +1 -0
  6. package/dist/cli/index.d.ts +7 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +159 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/client.d.ts +221 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +310 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/document/client.d.ts +57 -0
  15. package/dist/document/client.d.ts.map +1 -0
  16. package/dist/document/client.js +176 -0
  17. package/dist/document/client.js.map +1 -0
  18. package/dist/generator/document-generator.d.ts +50 -0
  19. package/dist/generator/document-generator.d.ts.map +1 -0
  20. package/dist/generator/document-generator.js +329 -0
  21. package/dist/generator/document-generator.js.map +1 -0
  22. package/dist/generator/index.d.ts +121 -0
  23. package/dist/generator/index.d.ts.map +1 -0
  24. package/dist/generator/index.js +559 -0
  25. package/dist/generator/index.js.map +1 -0
  26. package/dist/http/client.d.ts +117 -0
  27. package/dist/http/client.d.ts.map +1 -0
  28. package/dist/http/client.js +411 -0
  29. package/dist/http/client.js.map +1 -0
  30. package/dist/index.d.ts +22 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +21 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/media/helper.d.ts +89 -0
  35. package/dist/media/helper.d.ts.map +1 -0
  36. package/dist/media/helper.js +197 -0
  37. package/dist/media/helper.js.map +1 -0
  38. package/dist/media/url-builder.d.ts +142 -0
  39. package/dist/media/url-builder.d.ts.map +1 -0
  40. package/dist/media/url-builder.js +280 -0
  41. package/dist/media/url-builder.js.map +1 -0
  42. package/dist/query/builder.d.ts +270 -0
  43. package/dist/query/builder.d.ts.map +1 -0
  44. package/dist/query/builder.js +731 -0
  45. package/dist/query/builder.js.map +1 -0
  46. package/dist/query/index.d.ts +6 -0
  47. package/dist/query/index.d.ts.map +1 -0
  48. package/dist/query/index.js +5 -0
  49. package/dist/query/index.js.map +1 -0
  50. package/dist/react/hooks.d.ts +42 -0
  51. package/dist/react/hooks.d.ts.map +1 -0
  52. package/dist/react/hooks.js +59 -0
  53. package/dist/react/hooks.js.map +1 -0
  54. package/dist/react/index.d.ts +6 -0
  55. package/dist/react/index.d.ts.map +1 -0
  56. package/dist/react/index.js +6 -0
  57. package/dist/react/index.js.map +1 -0
  58. package/dist/server/index.d.ts +9 -0
  59. package/dist/server/index.d.ts.map +1 -0
  60. package/dist/server/index.js +8 -0
  61. package/dist/server/index.js.map +1 -0
  62. package/dist/server/media-proxy.d.ts +102 -0
  63. package/dist/server/media-proxy.d.ts.map +1 -0
  64. package/dist/server/media-proxy.js +160 -0
  65. package/dist/server/media-proxy.js.map +1 -0
  66. package/dist/shortcodes/index.d.ts +11 -0
  67. package/dist/shortcodes/index.d.ts.map +1 -0
  68. package/dist/shortcodes/index.js +10 -0
  69. package/dist/shortcodes/index.js.map +1 -0
  70. package/dist/shortcodes/parser.d.ts +32 -0
  71. package/dist/shortcodes/parser.d.ts.map +1 -0
  72. package/dist/shortcodes/parser.js +88 -0
  73. package/dist/shortcodes/parser.js.map +1 -0
  74. package/dist/shortcodes/resolver.d.ts +57 -0
  75. package/dist/shortcodes/resolver.d.ts.map +1 -0
  76. package/dist/shortcodes/resolver.js +97 -0
  77. package/dist/shortcodes/resolver.js.map +1 -0
  78. package/dist/shortcodes/types.d.ts +17 -0
  79. package/dist/shortcodes/types.d.ts.map +1 -0
  80. package/dist/shortcodes/types.js +6 -0
  81. package/dist/shortcodes/types.js.map +1 -0
  82. package/dist/types/index.d.ts +134 -0
  83. package/dist/types/index.d.ts.map +1 -0
  84. package/dist/types/index.js +6 -0
  85. package/dist/types/index.js.map +1 -0
  86. package/package.json +101 -0
@@ -0,0 +1,731 @@
1
+ /**
2
+ * Fluent Query Builder
3
+ * Provides a chainable API for building document queries with reference expansion
4
+ */
5
+ /**
6
+ * Fluent query builder for Trokky documents
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const articles = await client
11
+ * .from('article')
12
+ * .published()
13
+ * .expand('category')
14
+ * .expand('author')
15
+ * .sort({ _createdAt: 'desc' })
16
+ * .limit(10)
17
+ * .fetch()
18
+ * ```
19
+ */
20
+ export class QueryBuilder {
21
+ constructor(http, cache, type) {
22
+ this.http = http;
23
+ this.cache = cache;
24
+ this._filter = {};
25
+ this._sort = {};
26
+ this._expand = [];
27
+ this._options = { useCache: true };
28
+ this._type = type;
29
+ }
30
+ /**
31
+ * Filter by published status (_status === 'published')
32
+ */
33
+ published() {
34
+ this._filter._status = 'published';
35
+ return this;
36
+ }
37
+ /**
38
+ * Filter by draft status (_status === 'draft')
39
+ */
40
+ draft() {
41
+ this._filter._status = 'draft';
42
+ return this;
43
+ }
44
+ where(conditionsOrField, value) {
45
+ if (typeof conditionsOrField === 'string') {
46
+ this._filter[conditionsOrField] = value;
47
+ }
48
+ else {
49
+ this._filter = { ...this._filter, ...conditionsOrField };
50
+ }
51
+ return this;
52
+ }
53
+ /**
54
+ * Add equality filter
55
+ */
56
+ eq(field, value) {
57
+ this._filter[field] = value;
58
+ return this;
59
+ }
60
+ /**
61
+ * Add not-equal filter
62
+ */
63
+ neq(field, value) {
64
+ this._filter[field] = { $ne: value };
65
+ return this;
66
+ }
67
+ /**
68
+ * Add greater-than filter
69
+ */
70
+ gt(field, value) {
71
+ this._filter[field] = { $gt: value };
72
+ return this;
73
+ }
74
+ /**
75
+ * Add greater-than-or-equal filter
76
+ */
77
+ gte(field, value) {
78
+ this._filter[field] = { $gte: value };
79
+ return this;
80
+ }
81
+ /**
82
+ * Add less-than filter
83
+ */
84
+ lt(field, value) {
85
+ this._filter[field] = { $lt: value };
86
+ return this;
87
+ }
88
+ /**
89
+ * Add less-than-or-equal filter
90
+ */
91
+ lte(field, value) {
92
+ this._filter[field] = { $lte: value };
93
+ return this;
94
+ }
95
+ /**
96
+ * Add in-array filter
97
+ */
98
+ in(field, values) {
99
+ this._filter[field] = { $in: values };
100
+ return this;
101
+ }
102
+ /**
103
+ * Add not-in-array filter
104
+ */
105
+ notIn(field, values) {
106
+ this._filter[field] = { $nin: values };
107
+ return this;
108
+ }
109
+ /**
110
+ * Add text search filter
111
+ */
112
+ search(term, fields) {
113
+ this._filter.$text = { $search: term };
114
+ if (fields) {
115
+ this._filter.$text.$searchFields = fields;
116
+ }
117
+ return this;
118
+ }
119
+ /**
120
+ * Expand a reference field (resolve the reference to full document)
121
+ * @param field Field name to expand. Use 'field[]' syntax for array references
122
+ */
123
+ expand(field) {
124
+ const isArray = field.endsWith('[]');
125
+ const cleanField = isArray ? field.slice(0, -2) : field;
126
+ this._expand.push({
127
+ field: cleanField,
128
+ isArray
129
+ });
130
+ return this;
131
+ }
132
+ /**
133
+ * Set sort order
134
+ * @param sortConfig Sort configuration object
135
+ */
136
+ sort(sortConfig) {
137
+ this._sort = { ...this._sort, ...sortConfig };
138
+ return this;
139
+ }
140
+ /**
141
+ * Sort by field ascending
142
+ */
143
+ sortAsc(field) {
144
+ this._sort[field] = 'asc';
145
+ return this;
146
+ }
147
+ /**
148
+ * Sort by field descending
149
+ */
150
+ sortDesc(field) {
151
+ this._sort[field] = 'desc';
152
+ return this;
153
+ }
154
+ /**
155
+ * Sort by creation date (newest first)
156
+ */
157
+ newest() {
158
+ this._sort._createdAt = 'desc';
159
+ return this;
160
+ }
161
+ /**
162
+ * Sort by creation date (oldest first)
163
+ */
164
+ oldest() {
165
+ this._sort._createdAt = 'asc';
166
+ return this;
167
+ }
168
+ /**
169
+ * Limit number of results
170
+ */
171
+ limit(count) {
172
+ this._limit = count;
173
+ return this;
174
+ }
175
+ /**
176
+ * Skip first N results
177
+ */
178
+ offset(count) {
179
+ this._offset = count;
180
+ return this;
181
+ }
182
+ /**
183
+ * Skip first N results (alias for offset)
184
+ */
185
+ skip(count) {
186
+ return this.offset(count);
187
+ }
188
+ /**
189
+ * Select specific fields only
190
+ */
191
+ select(...fields) {
192
+ this._select = fields;
193
+ return this;
194
+ }
195
+ /**
196
+ * Disable caching for this query
197
+ */
198
+ noCache() {
199
+ this._options.useCache = false;
200
+ return this;
201
+ }
202
+ /**
203
+ * Add cache-busting timestamp to prevent stale data
204
+ */
205
+ fresh() {
206
+ this._options.noTimestamp = false;
207
+ this._options.useCache = false;
208
+ return this;
209
+ }
210
+ /**
211
+ * Execute the query and return results
212
+ */
213
+ async fetch() {
214
+ const result = await this.fetchWithMeta();
215
+ return result.documents;
216
+ }
217
+ /**
218
+ * Execute the query and return results with metadata
219
+ */
220
+ async fetchWithMeta() {
221
+ // Build query options
222
+ const options = {};
223
+ if (Object.keys(this._filter).length > 0) {
224
+ options.filter = this._filter;
225
+ }
226
+ if (Object.keys(this._sort).length > 0) {
227
+ options.sort = this._sort;
228
+ }
229
+ if (this._limit !== undefined) {
230
+ options.limit = this._limit;
231
+ }
232
+ if (this._offset !== undefined) {
233
+ options.offset = this._offset;
234
+ }
235
+ if (this._select) {
236
+ options.select = this._select;
237
+ }
238
+ // Build cache key
239
+ const cacheKey = this.cache.generateKey(`collections/${this._type}`, {
240
+ ...options,
241
+ expand: this._expand.map(e => e.field)
242
+ });
243
+ // Check cache
244
+ if (this._options.useCache) {
245
+ const cached = this.cache.get(cacheKey);
246
+ if (cached) {
247
+ return {
248
+ ...cached,
249
+ documents: cached.data || []
250
+ };
251
+ }
252
+ }
253
+ // Build query params
254
+ const queryParams = new URLSearchParams();
255
+ if (options.filter) {
256
+ queryParams.set('filter', JSON.stringify(options.filter));
257
+ }
258
+ if (options.sort && Object.keys(options.sort).length > 0) {
259
+ Object.entries(options.sort).forEach(([field, direction]) => {
260
+ const dir = direction === -1 || direction === 'desc' ? 'desc' : 'asc';
261
+ queryParams.append('sort', `${field}.${dir}`);
262
+ });
263
+ }
264
+ if (options.limit) {
265
+ queryParams.set('limit', options.limit.toString());
266
+ }
267
+ if (options.offset) {
268
+ queryParams.set('offset', options.offset.toString());
269
+ }
270
+ if (options.select) {
271
+ queryParams.set('select', options.select.join(','));
272
+ }
273
+ // Add server-side expansion if configured
274
+ if (this._expand.length > 0) {
275
+ const expandFields = this._expand.map(e => e.isArray ? `${e.field}[]` : e.field);
276
+ queryParams.set('expand', expandFields.join(','));
277
+ }
278
+ // Add timestamp for fresh queries
279
+ if (!this._options.noTimestamp && !this._options.useCache) {
280
+ queryParams.set('_t', Date.now().toString());
281
+ }
282
+ // Execute query
283
+ const endpoint = `/collections/${this._type}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
284
+ const result = await this.http.get(endpoint);
285
+ // Extract documents from result
286
+ let documents = result.data || [];
287
+ // Note: References are now expanded server-side via the expand query param
288
+ // Client-side expansion is kept as fallback for older API versions
289
+ if (this._expand.length > 0 && documents.length > 0 && !this.hasExpandedReferences(documents)) {
290
+ documents = await this.expandReferences(documents);
291
+ }
292
+ // Cache result
293
+ if (this._options.useCache) {
294
+ this.cache.set(cacheKey, result);
295
+ }
296
+ return {
297
+ ...result,
298
+ documents
299
+ };
300
+ }
301
+ /**
302
+ * Get first result or null
303
+ */
304
+ async first() {
305
+ this._limit = 1;
306
+ const results = await this.fetch();
307
+ return results[0] || null;
308
+ }
309
+ /**
310
+ * Get count of matching documents
311
+ */
312
+ async count() {
313
+ const queryParams = new URLSearchParams();
314
+ if (Object.keys(this._filter).length > 0) {
315
+ queryParams.set('filter', JSON.stringify(this._filter));
316
+ }
317
+ const endpoint = `/collections/${this._type}/count${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
318
+ const result = await this.http.get(endpoint);
319
+ return result.count;
320
+ }
321
+ /**
322
+ * Check if any documents match the query
323
+ */
324
+ async exists() {
325
+ const count = await this.count();
326
+ return count > 0;
327
+ }
328
+ /**
329
+ * Check if references have already been expanded by the server
330
+ */
331
+ hasExpandedReferences(documents) {
332
+ if (documents.length === 0)
333
+ return false;
334
+ const doc = documents[0];
335
+ for (const expandConfig of this._expand) {
336
+ const fieldValue = this.getNestedValue(doc, expandConfig.field);
337
+ if (fieldValue) {
338
+ // If it's an array, check first item
339
+ if (expandConfig.isArray && Array.isArray(fieldValue) && fieldValue.length > 0) {
340
+ const firstItem = fieldValue[0];
341
+ // If it has _id, it's been expanded (not just a reference with _ref)
342
+ if (firstItem && typeof firstItem === 'object' && '_id' in firstItem) {
343
+ return true;
344
+ }
345
+ }
346
+ else if (typeof fieldValue === 'object' && '_id' in fieldValue && !('_ref' in fieldValue)) {
347
+ // Single reference that's been expanded (has _id but no _ref)
348
+ return true;
349
+ }
350
+ }
351
+ }
352
+ return false;
353
+ }
354
+ /**
355
+ * Expand references in documents
356
+ */
357
+ async expandReferences(documents) {
358
+ // Collect all references to fetch
359
+ const referencesToFetch = new Map();
360
+ for (const doc of documents) {
361
+ for (const expandConfig of this._expand) {
362
+ const fieldValue = this.getNestedValue(doc, expandConfig.field);
363
+ if (!fieldValue)
364
+ continue;
365
+ if (expandConfig.isArray && Array.isArray(fieldValue)) {
366
+ // Array of references
367
+ for (const ref of fieldValue) {
368
+ if (this.isReference(ref)) {
369
+ const type = ref._type || this.inferTypeFromRef(ref._ref);
370
+ if (!referencesToFetch.has(type)) {
371
+ referencesToFetch.set(type, new Set());
372
+ }
373
+ referencesToFetch.get(type).add(ref._ref);
374
+ }
375
+ }
376
+ }
377
+ else if (this.isReference(fieldValue)) {
378
+ // Single reference
379
+ const type = fieldValue._type || this.inferTypeFromRef(fieldValue._ref);
380
+ if (!referencesToFetch.has(type)) {
381
+ referencesToFetch.set(type, new Set());
382
+ }
383
+ referencesToFetch.get(type).add(fieldValue._ref);
384
+ }
385
+ }
386
+ }
387
+ // Fetch all referenced documents in parallel
388
+ const resolvedRefs = new Map();
389
+ const fetchPromises = [];
390
+ for (const [type, ids] of referencesToFetch) {
391
+ for (const id of ids) {
392
+ const promise = this.fetchReference(type, id)
393
+ .then(doc => {
394
+ if (doc) {
395
+ resolvedRefs.set(`${type}:${id}`, doc);
396
+ }
397
+ })
398
+ .catch(err => {
399
+ console.warn(`Failed to resolve reference ${type}/${id}:`, err);
400
+ });
401
+ fetchPromises.push(promise);
402
+ }
403
+ }
404
+ await Promise.all(fetchPromises);
405
+ // Replace references with resolved documents
406
+ return documents.map(doc => {
407
+ const expanded = JSON.parse(JSON.stringify(doc));
408
+ for (const expandConfig of this._expand) {
409
+ const fieldValue = this.getNestedValue(expanded, expandConfig.field);
410
+ if (!fieldValue)
411
+ continue;
412
+ if (expandConfig.isArray && Array.isArray(fieldValue)) {
413
+ const expandedArray = fieldValue.map((ref) => {
414
+ if (this.isReference(ref)) {
415
+ const type = ref._type || this.inferTypeFromRef(ref._ref);
416
+ const resolved = resolvedRefs.get(`${type}:${ref._ref}`);
417
+ return resolved || ref;
418
+ }
419
+ return ref;
420
+ });
421
+ this.setNestedValue(expanded, expandConfig.field, expandedArray);
422
+ }
423
+ else if (this.isReference(fieldValue)) {
424
+ const type = fieldValue._type || this.inferTypeFromRef(fieldValue._ref);
425
+ const resolved = resolvedRefs.get(`${type}:${fieldValue._ref}`);
426
+ if (resolved) {
427
+ this.setNestedValue(expanded, expandConfig.field, resolved);
428
+ }
429
+ }
430
+ }
431
+ return expanded;
432
+ });
433
+ }
434
+ /**
435
+ * Check if a value is a reference object
436
+ */
437
+ isReference(value) {
438
+ return value && typeof value === 'object' && '_ref' in value;
439
+ }
440
+ /**
441
+ * Get a nested value from an object using dot notation path
442
+ * @param obj The object to traverse
443
+ * @param path Dot-separated path (e.g., 'documentation.featuredDocument')
444
+ */
445
+ getNestedValue(obj, path) {
446
+ return path.split('.').reduce((current, key) => current?.[key], obj);
447
+ }
448
+ /**
449
+ * Set a nested value in an object using dot notation path
450
+ * @param obj The object to modify
451
+ * @param path Dot-separated path (e.g., 'documentation.featuredDocument')
452
+ * @param value The value to set
453
+ */
454
+ setNestedValue(obj, path, value) {
455
+ const keys = path.split('.');
456
+ const lastKey = keys.pop();
457
+ const target = keys.reduce((current, key) => (current[key] ?? (current[key] = {})), obj);
458
+ target[lastKey] = value;
459
+ }
460
+ /**
461
+ * Infer type from reference ID (e.g., 'category-abc123' -> 'category')
462
+ */
463
+ inferTypeFromRef(ref) {
464
+ // Try to extract type from ref format like 'category-abc123'
465
+ const match = ref.match(/^([a-z-]+)-/);
466
+ return match ? match[1] : 'document';
467
+ }
468
+ /**
469
+ * Extract document from API response
470
+ */
471
+ extractDocument(result) {
472
+ if (!result)
473
+ return null;
474
+ // Handle direct document (has _id at top level)
475
+ if (result._id) {
476
+ return result;
477
+ }
478
+ // Handle { data: { ... } } wrapper
479
+ if (result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
480
+ return this.extractDocument(result.data);
481
+ }
482
+ // Handle { data: [...] } wrapper (take first item)
483
+ if (result.data && Array.isArray(result.data)) {
484
+ return result.data[0] || null;
485
+ }
486
+ // Handle { [collectionName]: { _id: "...", ... } } wrapper (e.g., { document: { _id: "..." } })
487
+ // This occurs when the API returns the document wrapped in a property named after the collection
488
+ const keys = Object.keys(result);
489
+ if (keys.length === 1) {
490
+ const wrappedValue = result[keys[0]];
491
+ if (wrappedValue && typeof wrappedValue === 'object' && wrappedValue._id) {
492
+ return wrappedValue;
493
+ }
494
+ }
495
+ // Fallback: return as-is
496
+ return result;
497
+ }
498
+ /**
499
+ * Fetch a single referenced document
500
+ */
501
+ async fetchReference(type, id) {
502
+ try {
503
+ const result = await this.http.get(`/collections/${type}/${id}`);
504
+ return this.extractDocument(result);
505
+ }
506
+ catch (error) {
507
+ // Try without type prefix in ID
508
+ if (id.startsWith(`${type}-`)) {
509
+ const cleanId = id.replace(`${type}-`, '');
510
+ try {
511
+ const result = await this.http.get(`/collections/${type}/${cleanId}`);
512
+ return this.extractDocument(result);
513
+ }
514
+ catch {
515
+ return null;
516
+ }
517
+ }
518
+ return null;
519
+ }
520
+ }
521
+ }
522
+ /**
523
+ * Singleton query builder for fetching a single document by type
524
+ */
525
+ export class SingletonBuilder {
526
+ constructor(http, cache, _type, _id) {
527
+ this.http = http;
528
+ this.cache = cache;
529
+ this._type = _type;
530
+ this._id = _id;
531
+ this._expand = [];
532
+ this._options = { useCache: false };
533
+ }
534
+ /**
535
+ * Expand a reference field
536
+ */
537
+ expand(field) {
538
+ const isArray = field.endsWith('[]');
539
+ const cleanField = isArray ? field.slice(0, -2) : field;
540
+ this._expand.push({
541
+ field: cleanField,
542
+ isArray
543
+ });
544
+ return this;
545
+ }
546
+ /**
547
+ * Disable caching
548
+ */
549
+ noCache() {
550
+ this._options.useCache = false;
551
+ return this;
552
+ }
553
+ /**
554
+ * Fetch fresh data with cache busting
555
+ */
556
+ fresh() {
557
+ this._options.useCache = false;
558
+ return this;
559
+ }
560
+ /**
561
+ * Execute and return the singleton document
562
+ */
563
+ async fetch() {
564
+ const queryParams = new URLSearchParams();
565
+ if (!this._options.useCache) {
566
+ queryParams.set('_t', Date.now().toString());
567
+ }
568
+ // Add server-side expansion if configured
569
+ if (this._expand.length > 0) {
570
+ const expandFields = this._expand.map(e => e.isArray ? `${e.field}[]` : e.field);
571
+ queryParams.set('expand', expandFields.join(','));
572
+ }
573
+ // For singletons, try specific ID first, then fall back to collection query
574
+ let endpoint;
575
+ if (this._id) {
576
+ endpoint = `/collections/${this._type}/${this._id}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
577
+ }
578
+ else {
579
+ // Query the collection and get first result
580
+ queryParams.set('limit', '1');
581
+ endpoint = `/collections/${this._type}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
582
+ }
583
+ try {
584
+ const result = await this.http.get(endpoint);
585
+ // Handle collection response vs single document
586
+ let doc = null;
587
+ if (result.data && Array.isArray(result.data)) {
588
+ doc = result.data[0] || null;
589
+ }
590
+ else if (result.data) {
591
+ doc = result.data;
592
+ }
593
+ else if (result._id) {
594
+ doc = result;
595
+ }
596
+ // Check if published (for singleton pages)
597
+ if (doc && doc._status && doc._status !== 'published') {
598
+ return null;
599
+ }
600
+ // Note: References are now expanded server-side via the expand query param
601
+ // Client-side expansion is kept as fallback for older API versions
602
+ if (doc && this._expand.length > 0 && !this.hasExpandedReferences(doc)) {
603
+ doc = await this.expandReferences(doc);
604
+ }
605
+ return doc;
606
+ }
607
+ catch (error) {
608
+ console.warn(`Failed to fetch singleton ${this._type}:`, error);
609
+ return null;
610
+ }
611
+ }
612
+ /**
613
+ * Check if references have already been expanded by the server
614
+ */
615
+ hasExpandedReferences(doc) {
616
+ for (const expandConfig of this._expand) {
617
+ const fieldValue = this.getNestedValue(doc, expandConfig.field);
618
+ if (fieldValue) {
619
+ // If it's an array, check first item
620
+ if (expandConfig.isArray && Array.isArray(fieldValue) && fieldValue.length > 0) {
621
+ const firstItem = fieldValue[0];
622
+ // If it has _id, it's been expanded (not just a reference with _ref)
623
+ if (firstItem && typeof firstItem === 'object' && '_id' in firstItem) {
624
+ return true;
625
+ }
626
+ }
627
+ else if (typeof fieldValue === 'object' && '_id' in fieldValue && !('_ref' in fieldValue)) {
628
+ // Single reference that's been expanded (has _id but no _ref)
629
+ return true;
630
+ }
631
+ }
632
+ }
633
+ return false;
634
+ }
635
+ /**
636
+ * Expand references in a single document
637
+ */
638
+ async expandReferences(doc) {
639
+ const expanded = JSON.parse(JSON.stringify(doc));
640
+ for (const expandConfig of this._expand) {
641
+ const fieldValue = this.getNestedValue(expanded, expandConfig.field);
642
+ if (!fieldValue)
643
+ continue;
644
+ if (expandConfig.isArray && Array.isArray(fieldValue)) {
645
+ const expandedArray = await Promise.all(fieldValue.map(async (ref) => {
646
+ if (this.isReference(ref)) {
647
+ const type = ref._type || this.inferTypeFromRef(ref._ref);
648
+ return await this.fetchReference(type, ref._ref) || ref;
649
+ }
650
+ return ref;
651
+ }));
652
+ this.setNestedValue(expanded, expandConfig.field, expandedArray);
653
+ }
654
+ else if (this.isReference(fieldValue)) {
655
+ const type = fieldValue._type || this.inferTypeFromRef(fieldValue._ref);
656
+ const resolved = await this.fetchReference(type, fieldValue._ref);
657
+ if (resolved) {
658
+ this.setNestedValue(expanded, expandConfig.field, resolved);
659
+ }
660
+ }
661
+ }
662
+ return expanded;
663
+ }
664
+ isReference(value) {
665
+ return value && typeof value === 'object' && '_ref' in value;
666
+ }
667
+ /**
668
+ * Get a nested value from an object using dot notation path
669
+ * @param obj The object to traverse
670
+ * @param path Dot-separated path (e.g., 'documentation.featuredDocument')
671
+ */
672
+ getNestedValue(obj, path) {
673
+ return path.split('.').reduce((current, key) => current?.[key], obj);
674
+ }
675
+ /**
676
+ * Set a nested value in an object using dot notation path
677
+ * @param obj The object to modify
678
+ * @param path Dot-separated path (e.g., 'documentation.featuredDocument')
679
+ * @param value The value to set
680
+ */
681
+ setNestedValue(obj, path, value) {
682
+ const keys = path.split('.');
683
+ const lastKey = keys.pop();
684
+ const target = keys.reduce((current, key) => (current[key] ?? (current[key] = {})), obj);
685
+ target[lastKey] = value;
686
+ }
687
+ inferTypeFromRef(ref) {
688
+ const match = ref.match(/^([a-z-]+)-/);
689
+ return match ? match[1] : 'document';
690
+ }
691
+ /**
692
+ * Extract document from API response
693
+ */
694
+ extractDocument(result) {
695
+ if (!result)
696
+ return null;
697
+ // Handle direct document (has _id at top level)
698
+ if (result._id) {
699
+ return result;
700
+ }
701
+ // Handle { data: { ... } } wrapper
702
+ if (result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
703
+ return this.extractDocument(result.data);
704
+ }
705
+ // Handle { data: [...] } wrapper (take first item)
706
+ if (result.data && Array.isArray(result.data)) {
707
+ return result.data[0] || null;
708
+ }
709
+ // Handle { [collectionName]: { _id: "...", ... } } wrapper (e.g., { document: { _id: "..." } })
710
+ // This occurs when the API returns the document wrapped in a property named after the collection
711
+ const keys = Object.keys(result);
712
+ if (keys.length === 1) {
713
+ const wrappedValue = result[keys[0]];
714
+ if (wrappedValue && typeof wrappedValue === 'object' && wrappedValue._id) {
715
+ return wrappedValue;
716
+ }
717
+ }
718
+ // Fallback: return as-is
719
+ return result;
720
+ }
721
+ async fetchReference(type, id) {
722
+ try {
723
+ const result = await this.http.get(`/collections/${type}/${id}`);
724
+ return this.extractDocument(result);
725
+ }
726
+ catch {
727
+ return null;
728
+ }
729
+ }
730
+ }
731
+ //# sourceMappingURL=builder.js.map