@nuskin/contentstack-lib 2.1.0-pa-1117.10 → 2.1.0-pa-1202.1
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/.releaserc +1 -1
- package/README.md +20 -0
- package/__tests__/api.spec.js +283 -18
- package/docs/CHANGELOG.md +1 -1
- package/package.json +1 -1
- package/src/api.js +189 -8
package/.releaserc
CHANGED
package/README.md
CHANGED
|
@@ -54,6 +54,26 @@ const [
|
|
|
54
54
|
mergeWithFallback: true
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
// Query a multiple-entry content type by one or more fields.
|
|
58
|
+
const shippingForms = await Stack.getMultiEntriesByFields(
|
|
59
|
+
'shipping_address_form',
|
|
60
|
+
{
|
|
61
|
+
country_code: 'US',
|
|
62
|
+
locale: 'en-us'
|
|
63
|
+
},
|
|
64
|
+
true
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Convenience wrapper when you only expect one result.
|
|
68
|
+
const shippingForm = await Stack.getMultiEntryByFields(
|
|
69
|
+
'shipping_address_form',
|
|
70
|
+
{
|
|
71
|
+
country_code: 'US',
|
|
72
|
+
locale: 'en-us'
|
|
73
|
+
},
|
|
74
|
+
true
|
|
75
|
+
);
|
|
76
|
+
|
|
57
77
|
// We could possibly add functionality to return the list of regions markets and languages in a single object structure
|
|
58
78
|
```
|
|
59
79
|
|
package/__tests__/api.spec.js
CHANGED
|
@@ -15,6 +15,10 @@ const {contentstack: prodEnvConfig} = require(`../config/prod.json`);
|
|
|
15
15
|
const mockPromiseResult = jest.fn();
|
|
16
16
|
const mockSinglePromiseResult = jest.fn();
|
|
17
17
|
const mockStack = jest.fn();
|
|
18
|
+
const mockContainedIn = jest.fn();
|
|
19
|
+
const mockWhere = jest.fn();
|
|
20
|
+
const mockIncludeReference = jest.fn();
|
|
21
|
+
const mockFetch = jest.fn();
|
|
18
22
|
|
|
19
23
|
function getConfig(env) {
|
|
20
24
|
let config = {};
|
|
@@ -42,34 +46,153 @@ function getConfig(env) {
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
beforeEach(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
find: mockPromiseResult,
|
|
52
|
-
findOne: mockSinglePromiseResult
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
}),
|
|
56
|
-
containedIn: () => ({
|
|
57
|
-
includeReference: () => ({
|
|
58
|
-
toJSON: () => ({
|
|
59
|
-
find: mockPromiseResult
|
|
60
|
-
})
|
|
61
|
-
})
|
|
49
|
+
const mockQuery = {
|
|
50
|
+
language: jest.fn(() => ({
|
|
51
|
+
includeFallback: () => ({
|
|
52
|
+
toJSON: () => ({
|
|
53
|
+
find: mockPromiseResult,
|
|
54
|
+
findOne: mockSinglePromiseResult
|
|
62
55
|
})
|
|
63
56
|
})
|
|
57
|
+
})),
|
|
58
|
+
containedIn: mockContainedIn,
|
|
59
|
+
where: mockWhere,
|
|
60
|
+
includeReference: mockIncludeReference,
|
|
61
|
+
toJSON: jest.fn(() => ({
|
|
62
|
+
find: mockPromiseResult
|
|
63
|
+
}))
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
mockContainedIn.mockImplementation(() => mockQuery);
|
|
67
|
+
mockWhere.mockImplementation(() => mockQuery);
|
|
68
|
+
mockIncludeReference.mockImplementation(() => mockQuery);
|
|
69
|
+
mockStack.mockImplementation(() => ({
|
|
70
|
+
ContentType: (contentType) => ({
|
|
71
|
+
Query: () => mockQuery,
|
|
72
|
+
fetch: () => mockFetch(contentType)
|
|
64
73
|
})
|
|
65
74
|
}));
|
|
66
75
|
|
|
76
|
+
mockFetch.mockImplementation((contentType) => {
|
|
77
|
+
const schemas = {
|
|
78
|
+
shipping_address_form: {
|
|
79
|
+
content_type: {
|
|
80
|
+
schema: [
|
|
81
|
+
{
|
|
82
|
+
uid: 'countries',
|
|
83
|
+
data_type: 'reference',
|
|
84
|
+
reference_to: 'market'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
uid: 'details',
|
|
88
|
+
data_type: 'group',
|
|
89
|
+
schema: [
|
|
90
|
+
{
|
|
91
|
+
uid: 'address_form',
|
|
92
|
+
data_type: 'reference',
|
|
93
|
+
reference_to: 'address_form'
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
uid: 'promo_blocks',
|
|
99
|
+
data_type: 'blocks',
|
|
100
|
+
blocks: [
|
|
101
|
+
{
|
|
102
|
+
uid: 'hero',
|
|
103
|
+
schema: [
|
|
104
|
+
{
|
|
105
|
+
uid: 'banner',
|
|
106
|
+
data_type: 'reference',
|
|
107
|
+
reference_to: 'banner'
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
address_form: {
|
|
117
|
+
content_type: {
|
|
118
|
+
schema: [
|
|
119
|
+
{
|
|
120
|
+
uid: 'address_elements',
|
|
121
|
+
data_type: 'reference',
|
|
122
|
+
reference_to: 'address_form_field'
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
market: {
|
|
128
|
+
content_type: {
|
|
129
|
+
schema: []
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
banner: {
|
|
133
|
+
content_type: {
|
|
134
|
+
schema: []
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
address_form_field: {
|
|
138
|
+
content_type: {
|
|
139
|
+
schema: [
|
|
140
|
+
{
|
|
141
|
+
uid: 'field_config',
|
|
142
|
+
data_type: 'reference',
|
|
143
|
+
reference_to: 'field_config'
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
field_config: {
|
|
149
|
+
content_type: {
|
|
150
|
+
schema: [
|
|
151
|
+
{
|
|
152
|
+
uid: 'validation_rule',
|
|
153
|
+
data_type: 'reference',
|
|
154
|
+
reference_to: 'validation_rule'
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
validation_rule: {
|
|
160
|
+
content_type: {
|
|
161
|
+
schema: [
|
|
162
|
+
{
|
|
163
|
+
uid: 'too_deep_reference',
|
|
164
|
+
data_type: 'reference',
|
|
165
|
+
reference_to: 'too_deep_reference'
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
too_deep_reference: {
|
|
171
|
+
content_type: {
|
|
172
|
+
schema: [
|
|
173
|
+
{
|
|
174
|
+
uid: 'should_not_be_included',
|
|
175
|
+
data_type: 'reference',
|
|
176
|
+
reference_to: 'market'
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return Promise.resolve(schemas[contentType] || {content_type: {schema: []}});
|
|
184
|
+
});
|
|
185
|
+
|
|
67
186
|
jest.mock("contentstack", () => ({Stack: mockStack}));
|
|
68
187
|
});
|
|
69
188
|
|
|
70
189
|
afterEach(() => {
|
|
71
190
|
mockSinglePromiseResult.mockReset();
|
|
72
191
|
mockPromiseResult.mockReset();
|
|
192
|
+
mockContainedIn.mockReset();
|
|
193
|
+
mockWhere.mockReset();
|
|
194
|
+
mockIncludeReference.mockReset();
|
|
195
|
+
mockFetch.mockReset();
|
|
73
196
|
//jest.resetAllMocks(); don't reset the mockStack function
|
|
74
197
|
});
|
|
75
198
|
|
|
@@ -78,7 +201,7 @@ const mockResults = () => {
|
|
|
78
201
|
mockPromiseResult.mockResolvedValueOnce(checkoutAdrStrings);
|
|
79
202
|
mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutAdrStrings);
|
|
80
203
|
mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutCartStrings);
|
|
81
|
-
}
|
|
204
|
+
};
|
|
82
205
|
|
|
83
206
|
describe("ContentstackApi", () => {
|
|
84
207
|
test("getSingletonEntries", async () => {
|
|
@@ -152,4 +275,146 @@ describe("ContentstackApi", () => {
|
|
|
152
275
|
await Stack.getSingletonEntries({contentTypeUIDs: ['checkout_adr_strings'], language: 'in'});
|
|
153
276
|
expect(mockSinglePromiseResult).toHaveBeenCalled();
|
|
154
277
|
});
|
|
278
|
+
|
|
279
|
+
test('getMultiEntries does not include references when includeRefs is false', async () => {
|
|
280
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
281
|
+
const {getStack} = require('../src/api');
|
|
282
|
+
const Stack = getStack('dev');
|
|
283
|
+
await Stack.getMultiEntries('market', false, ['United States']);
|
|
284
|
+
expect(mockContainedIn).toHaveBeenCalledWith('title', ['United States']);
|
|
285
|
+
expect(mockIncludeReference).not.toHaveBeenCalled();
|
|
286
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('getMultiEntries includes explicit reference paths', async () => {
|
|
290
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
291
|
+
const {getStack} = require('../src/api');
|
|
292
|
+
const Stack = getStack('dev');
|
|
293
|
+
await Stack.getMultiEntries('market', ['countries', 'address_form.address_elements'], ['United States']);
|
|
294
|
+
expect(mockIncludeReference).toHaveBeenCalledWith(['countries', 'address_form.address_elements']);
|
|
295
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('getMultiEntries discovers reference paths from schema when includeRefs is true', async () => {
|
|
299
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
300
|
+
const {getStack} = require('../src/api');
|
|
301
|
+
const Stack = getStack('dev');
|
|
302
|
+
await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
|
|
303
|
+
expect(mockFetch).toHaveBeenCalledTimes(6);
|
|
304
|
+
expect(mockIncludeReference).toHaveBeenCalledWith([
|
|
305
|
+
'countries',
|
|
306
|
+
'details.address_form',
|
|
307
|
+
'details.address_form.address_elements',
|
|
308
|
+
'details.address_form.address_elements.field_config',
|
|
309
|
+
'details.address_form.address_elements.field_config.validation_rule',
|
|
310
|
+
'promo_blocks.hero.banner'
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('getMultiEntries caches discovered reference paths per content type', async () => {
|
|
315
|
+
mockPromiseResult.mockResolvedValue([[{title: 'United States'}]]);
|
|
316
|
+
const {getStack} = require('../src/api');
|
|
317
|
+
const Stack = getStack('test');
|
|
318
|
+
await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
|
|
319
|
+
await Stack.getMultiEntries('shipping_address_form', true, ['Canada']);
|
|
320
|
+
expect(mockFetch).toHaveBeenCalledTimes(6);
|
|
321
|
+
expect(mockIncludeReference).toHaveBeenNthCalledWith(1, [
|
|
322
|
+
'countries',
|
|
323
|
+
'details.address_form',
|
|
324
|
+
'details.address_form.address_elements',
|
|
325
|
+
'details.address_form.address_elements.field_config',
|
|
326
|
+
'details.address_form.address_elements.field_config.validation_rule',
|
|
327
|
+
'promo_blocks.hero.banner'
|
|
328
|
+
]);
|
|
329
|
+
expect(mockIncludeReference).toHaveBeenNthCalledWith(2, [
|
|
330
|
+
'countries',
|
|
331
|
+
'details.address_form',
|
|
332
|
+
'details.address_form.address_elements',
|
|
333
|
+
'details.address_form.address_elements.field_config',
|
|
334
|
+
'details.address_form.address_elements.field_config.validation_rule',
|
|
335
|
+
'promo_blocks.hero.banner'
|
|
336
|
+
]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('getMultiEntries limits automatic reference traversal to four levels deep', async () => {
|
|
340
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
341
|
+
const {getStack} = require('../src/api');
|
|
342
|
+
const Stack = getStack('stage');
|
|
343
|
+
await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
|
|
344
|
+
expect(mockIncludeReference).toHaveBeenCalledWith([
|
|
345
|
+
'countries',
|
|
346
|
+
'details.address_form',
|
|
347
|
+
'details.address_form.address_elements',
|
|
348
|
+
'details.address_form.address_elements.field_config',
|
|
349
|
+
'details.address_form.address_elements.field_config.validation_rule',
|
|
350
|
+
'promo_blocks.hero.banner'
|
|
351
|
+
]);
|
|
352
|
+
expect(mockIncludeReference).not.toHaveBeenCalledWith(expect.arrayContaining([
|
|
353
|
+
'details.address_form.address_elements.field_config.validation_rule.too_deep_reference',
|
|
354
|
+
'details.address_form.address_elements.field_config.validation_rule.too_deep_reference.should_not_be_included'
|
|
355
|
+
]));
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('getMultiEntriesByFields applies where filters for scalar values', async () => {
|
|
359
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
360
|
+
const {getStack} = require('../src/api');
|
|
361
|
+
const Stack = getStack('prod');
|
|
362
|
+
await Stack.getMultiEntriesByFields('market', {
|
|
363
|
+
country_code: 'US',
|
|
364
|
+
locale: 'en-us'
|
|
365
|
+
});
|
|
366
|
+
expect(mockWhere).toHaveBeenNthCalledWith(1, 'country_code', 'US');
|
|
367
|
+
expect(mockWhere).toHaveBeenNthCalledWith(2, 'locale', 'en-us');
|
|
368
|
+
expect(mockContainedIn).not.toHaveBeenCalled();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('getMultiEntriesByFields applies containedIn for array values', async () => {
|
|
372
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
373
|
+
const {getStack} = require('../src/api');
|
|
374
|
+
const Stack = getStack('dev');
|
|
375
|
+
await Stack.getMultiEntriesByFields('market', {
|
|
376
|
+
title: ['United States', 'Canada'],
|
|
377
|
+
locale: 'en-us'
|
|
378
|
+
});
|
|
379
|
+
expect(mockContainedIn).toHaveBeenCalledWith('title', ['United States', 'Canada']);
|
|
380
|
+
expect(mockWhere).toHaveBeenCalledWith('locale', 'en-us');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('getMultiEntriesByFields resolves include refs when requested', async () => {
|
|
384
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
385
|
+
const {getStack} = require('../src/api');
|
|
386
|
+
const Stack = getStack('stage');
|
|
387
|
+
await Stack.getMultiEntriesByFields('shipping_address_form', {
|
|
388
|
+
country_code: 'US',
|
|
389
|
+
locale: 'en-us'
|
|
390
|
+
}, true);
|
|
391
|
+
expect(mockIncludeReference).toHaveBeenCalledWith([
|
|
392
|
+
'countries',
|
|
393
|
+
'details.address_form',
|
|
394
|
+
'details.address_form.address_elements',
|
|
395
|
+
'details.address_form.address_elements.field_config',
|
|
396
|
+
'details.address_form.address_elements.field_config.validation_rule',
|
|
397
|
+
'promo_blocks.hero.banner'
|
|
398
|
+
]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('getMultiEntriesByFields throws when fields are missing', async () => {
|
|
402
|
+
const {getStack} = require('../src/api');
|
|
403
|
+
const Stack = getStack('dev');
|
|
404
|
+
await expect(Stack.getMultiEntriesByFields('market')).rejects.toStrictEqual(
|
|
405
|
+
new Error('Call to getMultiEntriesByFields: fields are required')
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('getMultiEntryByFields returns the first matching entry', async () => {
|
|
410
|
+
mockPromiseResult.mockResolvedValueOnce([[
|
|
411
|
+
{title: 'United States'},
|
|
412
|
+
{title: 'Canada'}
|
|
413
|
+
]]);
|
|
414
|
+
const {getStack} = require('../src/api');
|
|
415
|
+
const Stack = getStack('test');
|
|
416
|
+
await expect(Stack.getMultiEntryByFields('market', {
|
|
417
|
+
locale: 'en-us'
|
|
418
|
+
})).resolves.toStrictEqual({title: 'United States'});
|
|
419
|
+
});
|
|
155
420
|
});
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# [2.1.0-pa-
|
|
1
|
+
# [2.1.0-pa-1202.1](https://code.tls.nuskin.io/ns-am/utility/npm/contentstack-lib/compare/v2.0.2...v2.1.0-pa-1202.1) (2026-06-25)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuskin/contentstack-lib",
|
|
3
|
-
"version": "2.1.0-pa-
|
|
3
|
+
"version": "2.1.0-pa-1202.1",
|
|
4
4
|
"description": "This project contains configuration and api code to access Contentstack, to be shared between the backend (AWS Lambda) and frontend (Vue, etc).",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
package/src/api.js
CHANGED
|
@@ -8,6 +8,9 @@ const {contentstack: prodEnvConfig} = require(`../config/prod.json`);
|
|
|
8
8
|
|
|
9
9
|
let env = null;
|
|
10
10
|
let Stack = null;
|
|
11
|
+
const referenceFieldCache = new Map();
|
|
12
|
+
const contentTypeSchemaCache = new Map();
|
|
13
|
+
const MAX_REFERENCE_DEPTH = 4;
|
|
11
14
|
|
|
12
15
|
const COMMON_LANGUAGES = {
|
|
13
16
|
'da': 'da-dk',
|
|
@@ -173,6 +176,130 @@ async function getSingletonEntries(options) {
|
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
async function _getContentTypeSchema(contentType) {
|
|
180
|
+
if (contentTypeSchemaCache.has(contentType)) {
|
|
181
|
+
return contentTypeSchemaCache.get(contentType);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const contentTypeSchema = await Stack.ContentType(contentType).fetch();
|
|
185
|
+
const schema = contentTypeSchema.content_type.schema || [];
|
|
186
|
+
contentTypeSchemaCache.set(contentType, schema);
|
|
187
|
+
return schema;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function _collectReferenceFieldPathsForSchema(schema = [], parentPath = '', visited = new Set(), depth = 0) {
|
|
191
|
+
const allPaths = [];
|
|
192
|
+
|
|
193
|
+
for (const field of schema) {
|
|
194
|
+
const currentPath = parentPath ? `${parentPath}.${field.uid}` : field.uid;
|
|
195
|
+
|
|
196
|
+
if (Array.isArray(field.schema)) {
|
|
197
|
+
const nestedSchemaPaths = await _collectReferenceFieldPathsForSchema(field.schema, currentPath, visited, depth);
|
|
198
|
+
allPaths.push(...nestedSchemaPaths);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (field.data_type === 'blocks' && Array.isArray(field.blocks)) {
|
|
202
|
+
for (const block of field.blocks) {
|
|
203
|
+
if (Array.isArray(block.schema)) {
|
|
204
|
+
const blockPaths = await _collectReferenceFieldPathsForSchema(
|
|
205
|
+
block.schema,
|
|
206
|
+
`${currentPath}.${block.uid}`,
|
|
207
|
+
visited,
|
|
208
|
+
depth
|
|
209
|
+
);
|
|
210
|
+
allPaths.push(...blockPaths);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (field.data_type === 'reference') {
|
|
216
|
+
const currentDepth = depth + 1;
|
|
217
|
+
if (currentDepth > MAX_REFERENCE_DEPTH) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
allPaths.push(currentPath);
|
|
221
|
+
const referenceTargets = Array.isArray(field.reference_to) ?
|
|
222
|
+
field.reference_to :
|
|
223
|
+
field.reference_to ? [field.reference_to] : [];
|
|
224
|
+
|
|
225
|
+
if (currentDepth >= MAX_REFERENCE_DEPTH) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const referencedContentType of referenceTargets) {
|
|
230
|
+
const nestedReferencePaths = await _collectReferenceFieldPathsForContentType(
|
|
231
|
+
referencedContentType,
|
|
232
|
+
currentPath,
|
|
233
|
+
visited,
|
|
234
|
+
currentDepth
|
|
235
|
+
);
|
|
236
|
+
allPaths.push(...nestedReferencePaths);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return [...new Set(allPaths)];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function _collectReferenceFieldPathsForContentType(contentType, parentPath = '', visited = new Set(), depth = 0) {
|
|
245
|
+
const visitKey = `${contentType}|${parentPath}|${depth}`;
|
|
246
|
+
if (visited.has(visitKey)) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
visited.add(visitKey);
|
|
250
|
+
|
|
251
|
+
const schema = await _getContentTypeSchema(contentType);
|
|
252
|
+
return _collectReferenceFieldPathsForSchema(schema, parentPath, visited, depth);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolve reference include paths from a boolean, string, or array input.
|
|
257
|
+
* @param {string} contentType
|
|
258
|
+
* @param {boolean|string|string[]} includeRefs
|
|
259
|
+
* @return {Promise<string[]>}
|
|
260
|
+
* @private
|
|
261
|
+
*/
|
|
262
|
+
async function _resolveIncludeRefs(contentType, includeRefs) {
|
|
263
|
+
if (!includeRefs) {
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof includeRefs === 'string') {
|
|
268
|
+
return [includeRefs];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (Array.isArray(includeRefs)) {
|
|
272
|
+
return includeRefs;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (includeRefs !== true) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (referenceFieldCache.has(contentType)) {
|
|
280
|
+
return referenceFieldCache.get(contentType);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const referenceFields = await _collectReferenceFieldPathsForContentType(contentType);
|
|
284
|
+
referenceFieldCache.set(contentType, referenceFields);
|
|
285
|
+
return referenceFields;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Apply field filters to a query. Scalar values use where(), arrays use containedIn().
|
|
290
|
+
* @param {Object} query
|
|
291
|
+
* @param {Object<string, string|string[]>} fields
|
|
292
|
+
* @return {Object}
|
|
293
|
+
* @private
|
|
294
|
+
*/
|
|
295
|
+
function _applyFieldFilters(query, fields) {
|
|
296
|
+
return Object.entries(fields).reduce((currentQuery, [field, value]) => {
|
|
297
|
+
return Array.isArray(value) ?
|
|
298
|
+
currentQuery.containedIn(field, value) :
|
|
299
|
+
currentQuery.where(field, value);
|
|
300
|
+
}, query);
|
|
301
|
+
}
|
|
302
|
+
|
|
176
303
|
/**
|
|
177
304
|
* Gets all the specified entries for a multiple content type
|
|
178
305
|
* @param {string} contentType
|
|
@@ -181,19 +308,69 @@ async function getSingletonEntries(options) {
|
|
|
181
308
|
*/
|
|
182
309
|
const getMultiEntries = async (contentType, includeRefs, values) => {
|
|
183
310
|
try {
|
|
184
|
-
const
|
|
311
|
+
const resolvedIncludeRefs = await _resolveIncludeRefs(contentType, includeRefs);
|
|
312
|
+
|
|
313
|
+
let query = Stack
|
|
185
314
|
.ContentType(contentType)
|
|
186
315
|
.Query()
|
|
187
|
-
.containedIn('title', values)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.
|
|
316
|
+
.containedIn('title', values);
|
|
317
|
+
|
|
318
|
+
if (resolvedIncludeRefs.length) {
|
|
319
|
+
query = query.includeReference(resolvedIncludeRefs);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const [results] = await query.toJSON().find();
|
|
191
323
|
|
|
192
324
|
return results;
|
|
193
325
|
} catch(err) {
|
|
194
326
|
console.error(err);
|
|
195
327
|
}
|
|
196
|
-
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Gets entries for a multiple content type filtered by one or more fields.
|
|
332
|
+
* @param {string} contentType
|
|
333
|
+
* @param {Object<string, string|string[]>} fields
|
|
334
|
+
* @param {boolean|string|string[]=} includeRefs
|
|
335
|
+
* @return {Promise<Object[]>}
|
|
336
|
+
*/
|
|
337
|
+
const getMultiEntriesByFields = async (contentType, fields, includeRefs) => {
|
|
338
|
+
if (!fields || !Object.keys(fields).length) {
|
|
339
|
+
throw new Error('Call to getMultiEntriesByFields: fields are required');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const resolvedIncludeRefs = await _resolveIncludeRefs(contentType, includeRefs);
|
|
344
|
+
let query = _applyFieldFilters(
|
|
345
|
+
Stack
|
|
346
|
+
.ContentType(contentType)
|
|
347
|
+
.Query(),
|
|
348
|
+
fields
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (resolvedIncludeRefs.length) {
|
|
352
|
+
query = query.includeReference(resolvedIncludeRefs);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const [results] = await query.toJSON().find();
|
|
356
|
+
return results;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.error(err);
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gets the first matching entry for a multiple content type filtered by fields.
|
|
365
|
+
* @param {string} contentType
|
|
366
|
+
* @param {Object<string, string|string[]>} fields
|
|
367
|
+
* @param {boolean|string|string[]=} includeRefs
|
|
368
|
+
* @return {Promise<Object|undefined>}
|
|
369
|
+
*/
|
|
370
|
+
const getMultiEntryByFields = async (contentType, fields, includeRefs) => {
|
|
371
|
+
const results = await getMultiEntriesByFields(contentType, fields, includeRefs);
|
|
372
|
+
return results[0];
|
|
373
|
+
};
|
|
197
374
|
|
|
198
375
|
/**
|
|
199
376
|
* Returns contentstack Stack object initialized with the api key, deliveryToken, and extended with custom functionality
|
|
@@ -214,12 +391,16 @@ function getStack(envrnmnt) {
|
|
|
214
391
|
}
|
|
215
392
|
|
|
216
393
|
env = envrnmnt;
|
|
217
|
-
|
|
218
|
-
|
|
394
|
+
referenceFieldCache.clear();
|
|
395
|
+
contentTypeSchemaCache.clear();
|
|
396
|
+
const {apiKey: api_key, deliveryToken: delivery_token, environment} = getConfig(env);
|
|
397
|
+
Stack = Contentstack.Stack({api_key, delivery_token, environment});
|
|
219
398
|
|
|
220
399
|
// extend the Stack with a custom function to get translations
|
|
221
400
|
Stack.getSingletonEntries = getSingletonEntries;
|
|
222
401
|
Stack.getMultiEntries = getMultiEntries;
|
|
402
|
+
Stack.getMultiEntriesByFields = getMultiEntriesByFields;
|
|
403
|
+
Stack.getMultiEntryByFields = getMultiEntryByFields;
|
|
223
404
|
|
|
224
405
|
return Stack;
|
|
225
406
|
}
|