@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.
- package/README.md +833 -0
- package/dist/cache/manager.d.ts +47 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +100 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +159 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client.d.ts +221 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +310 -0
- package/dist/client.js.map +1 -0
- package/dist/document/client.d.ts +57 -0
- package/dist/document/client.d.ts.map +1 -0
- package/dist/document/client.js +176 -0
- package/dist/document/client.js.map +1 -0
- package/dist/generator/document-generator.d.ts +50 -0
- package/dist/generator/document-generator.d.ts.map +1 -0
- package/dist/generator/document-generator.js +329 -0
- package/dist/generator/document-generator.js.map +1 -0
- package/dist/generator/index.d.ts +121 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +559 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/http/client.d.ts +117 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +411 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/media/helper.d.ts +89 -0
- package/dist/media/helper.d.ts.map +1 -0
- package/dist/media/helper.js +197 -0
- package/dist/media/helper.js.map +1 -0
- package/dist/media/url-builder.d.ts +142 -0
- package/dist/media/url-builder.d.ts.map +1 -0
- package/dist/media/url-builder.js +280 -0
- package/dist/media/url-builder.js.map +1 -0
- package/dist/query/builder.d.ts +270 -0
- package/dist/query/builder.d.ts.map +1 -0
- package/dist/query/builder.js +731 -0
- package/dist/query/builder.js.map +1 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +5 -0
- package/dist/query/index.js.map +1 -0
- package/dist/react/hooks.d.ts +42 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +59 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/media-proxy.d.ts +102 -0
- package/dist/server/media-proxy.d.ts.map +1 -0
- package/dist/server/media-proxy.js +160 -0
- package/dist/server/media-proxy.js.map +1 -0
- package/dist/shortcodes/index.d.ts +11 -0
- package/dist/shortcodes/index.d.ts.map +1 -0
- package/dist/shortcodes/index.js +10 -0
- package/dist/shortcodes/index.js.map +1 -0
- package/dist/shortcodes/parser.d.ts +32 -0
- package/dist/shortcodes/parser.d.ts.map +1 -0
- package/dist/shortcodes/parser.js +88 -0
- package/dist/shortcodes/parser.js.map +1 -0
- package/dist/shortcodes/resolver.d.ts +57 -0
- package/dist/shortcodes/resolver.d.ts.map +1 -0
- package/dist/shortcodes/resolver.js +97 -0
- package/dist/shortcodes/resolver.js.map +1 -0
- package/dist/shortcodes/types.d.ts +17 -0
- package/dist/shortcodes/types.d.ts.map +1 -0
- package/dist/shortcodes/types.js +6 -0
- package/dist/shortcodes/types.js.map +1 -0
- package/dist/types/index.d.ts +134 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- 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
|