@nuskin/contentstack-lib 2.1.0-pa-1117.10 → 2.1.0-pa-1117.11
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/__tests__/api.spec.js +114 -18
- package/docs/CHANGELOG.md +1 -1
- package/package.json +1 -1
- package/src/api.js +78 -8
package/__tests__/api.spec.js
CHANGED
|
@@ -15,6 +15,9 @@ 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 mockIncludeReference = jest.fn();
|
|
20
|
+
const mockFetch = jest.fn();
|
|
18
21
|
|
|
19
22
|
function getConfig(env) {
|
|
20
23
|
let config = {};
|
|
@@ -42,34 +45,76 @@ function getConfig(env) {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
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
|
-
})
|
|
48
|
+
const mockQuery = {
|
|
49
|
+
language: jest.fn(() => ({
|
|
50
|
+
includeFallback: () => ({
|
|
51
|
+
toJSON: () => ({
|
|
52
|
+
find: mockPromiseResult,
|
|
53
|
+
findOne: mockSinglePromiseResult
|
|
62
54
|
})
|
|
63
55
|
})
|
|
56
|
+
})),
|
|
57
|
+
containedIn: mockContainedIn,
|
|
58
|
+
includeReference: mockIncludeReference,
|
|
59
|
+
toJSON: jest.fn(() => ({
|
|
60
|
+
find: mockPromiseResult
|
|
61
|
+
}))
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
mockContainedIn.mockImplementation(() => mockQuery);
|
|
65
|
+
mockIncludeReference.mockImplementation(() => mockQuery);
|
|
66
|
+
mockStack.mockImplementation(() => ({
|
|
67
|
+
ContentType: () => ({
|
|
68
|
+
Query: () => mockQuery,
|
|
69
|
+
fetch: mockFetch
|
|
64
70
|
})
|
|
65
71
|
}));
|
|
66
72
|
|
|
73
|
+
mockFetch.mockResolvedValue({
|
|
74
|
+
content_type: {
|
|
75
|
+
schema: [
|
|
76
|
+
{
|
|
77
|
+
uid: 'countries',
|
|
78
|
+
data_type: 'reference'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
uid: 'details',
|
|
82
|
+
data_type: 'group',
|
|
83
|
+
schema: [
|
|
84
|
+
{
|
|
85
|
+
uid: 'address_form',
|
|
86
|
+
data_type: 'reference'
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
uid: 'promo_blocks',
|
|
92
|
+
data_type: 'blocks',
|
|
93
|
+
blocks: [
|
|
94
|
+
{
|
|
95
|
+
uid: 'hero',
|
|
96
|
+
schema: [
|
|
97
|
+
{
|
|
98
|
+
uid: 'banner',
|
|
99
|
+
data_type: 'reference'
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
67
109
|
jest.mock("contentstack", () => ({Stack: mockStack}));
|
|
68
110
|
});
|
|
69
111
|
|
|
70
112
|
afterEach(() => {
|
|
71
113
|
mockSinglePromiseResult.mockReset();
|
|
72
114
|
mockPromiseResult.mockReset();
|
|
115
|
+
mockContainedIn.mockReset();
|
|
116
|
+
mockIncludeReference.mockReset();
|
|
117
|
+
mockFetch.mockReset();
|
|
73
118
|
//jest.resetAllMocks(); don't reset the mockStack function
|
|
74
119
|
});
|
|
75
120
|
|
|
@@ -78,7 +123,7 @@ const mockResults = () => {
|
|
|
78
123
|
mockPromiseResult.mockResolvedValueOnce(checkoutAdrStrings);
|
|
79
124
|
mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutAdrStrings);
|
|
80
125
|
mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutCartStrings);
|
|
81
|
-
}
|
|
126
|
+
};
|
|
82
127
|
|
|
83
128
|
describe("ContentstackApi", () => {
|
|
84
129
|
test("getSingletonEntries", async () => {
|
|
@@ -152,4 +197,55 @@ describe("ContentstackApi", () => {
|
|
|
152
197
|
await Stack.getSingletonEntries({contentTypeUIDs: ['checkout_adr_strings'], language: 'in'});
|
|
153
198
|
expect(mockSinglePromiseResult).toHaveBeenCalled();
|
|
154
199
|
});
|
|
200
|
+
|
|
201
|
+
test('getMultiEntries does not include references when includeRefs is false', async () => {
|
|
202
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
203
|
+
const {getStack} = require('../src/api');
|
|
204
|
+
const Stack = getStack('dev');
|
|
205
|
+
await Stack.getMultiEntries('market', false, ['United States']);
|
|
206
|
+
expect(mockContainedIn).toHaveBeenCalledWith('title', ['United States']);
|
|
207
|
+
expect(mockIncludeReference).not.toHaveBeenCalled();
|
|
208
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('getMultiEntries includes explicit reference paths', async () => {
|
|
212
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
213
|
+
const {getStack} = require('../src/api');
|
|
214
|
+
const Stack = getStack('dev');
|
|
215
|
+
await Stack.getMultiEntries('market', ['countries', 'address_form.address_elements'], ['United States']);
|
|
216
|
+
expect(mockIncludeReference).toHaveBeenCalledWith(['countries', 'address_form.address_elements']);
|
|
217
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('getMultiEntries discovers reference paths from schema when includeRefs is true', async () => {
|
|
221
|
+
mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
|
|
222
|
+
const {getStack} = require('../src/api');
|
|
223
|
+
const Stack = getStack('dev');
|
|
224
|
+
await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
|
|
225
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(mockIncludeReference).toHaveBeenCalledWith([
|
|
227
|
+
'countries',
|
|
228
|
+
'details.address_form',
|
|
229
|
+
'promo_blocks.hero.banner'
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('getMultiEntries caches discovered reference paths per content type', async () => {
|
|
234
|
+
mockPromiseResult.mockResolvedValue([[{title: 'United States'}]]);
|
|
235
|
+
const {getStack} = require('../src/api');
|
|
236
|
+
const Stack = getStack('test');
|
|
237
|
+
await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
|
|
238
|
+
await Stack.getMultiEntries('shipping_address_form', true, ['Canada']);
|
|
239
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
240
|
+
expect(mockIncludeReference).toHaveBeenNthCalledWith(1, [
|
|
241
|
+
'countries',
|
|
242
|
+
'details.address_form',
|
|
243
|
+
'promo_blocks.hero.banner'
|
|
244
|
+
]);
|
|
245
|
+
expect(mockIncludeReference).toHaveBeenNthCalledWith(2, [
|
|
246
|
+
'countries',
|
|
247
|
+
'details.address_form',
|
|
248
|
+
'promo_blocks.hero.banner'
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
155
251
|
});
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
# [2.1.0-pa-1117.
|
|
1
|
+
# [2.1.0-pa-1117.11](https://code.tls.nuskin.io/ns-am/utility/npm/contentstack-lib/compare/v2.1.0-pa-1117.10...v2.1.0-pa-1117.11) (2026-05-21)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuskin/contentstack-lib",
|
|
3
|
-
"version": "2.1.0-pa-1117.
|
|
3
|
+
"version": "2.1.0-pa-1117.11",
|
|
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,7 @@ const {contentstack: prodEnvConfig} = require(`../config/prod.json`);
|
|
|
8
8
|
|
|
9
9
|
let env = null;
|
|
10
10
|
let Stack = null;
|
|
11
|
+
const referenceFieldCache = new Map();
|
|
11
12
|
|
|
12
13
|
const COMMON_LANGUAGES = {
|
|
13
14
|
'da': 'da-dk',
|
|
@@ -173,6 +174,69 @@ async function getSingletonEntries(options) {
|
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Collect reference field paths recursively from a content type schema.
|
|
179
|
+
* @param {Object[]} schema
|
|
180
|
+
* @param {string=} parentPath
|
|
181
|
+
* @return {string[]}
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
function _collectReferenceFieldPaths(schema = [], parentPath = '') {
|
|
185
|
+
return schema.flatMap((field) => {
|
|
186
|
+
const currentPath = parentPath ? `${parentPath}.${field.uid}` : field.uid;
|
|
187
|
+
const nestedSchemaPaths = Array.isArray(field.schema) ?
|
|
188
|
+
_collectReferenceFieldPaths(field.schema, currentPath) :
|
|
189
|
+
[];
|
|
190
|
+
const blockPaths = field.data_type === 'blocks' && Array.isArray(field.blocks) ?
|
|
191
|
+
field.blocks.flatMap((block) => {
|
|
192
|
+
return Array.isArray(block.schema) ?
|
|
193
|
+
_collectReferenceFieldPaths(block.schema, `${currentPath}.${block.uid}`) :
|
|
194
|
+
[];
|
|
195
|
+
}) :
|
|
196
|
+
[];
|
|
197
|
+
|
|
198
|
+
return [
|
|
199
|
+
...(field.data_type === 'reference' ? [currentPath] : []),
|
|
200
|
+
...nestedSchemaPaths,
|
|
201
|
+
...blockPaths
|
|
202
|
+
];
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve reference include paths from a boolean, string, or array input.
|
|
208
|
+
* @param {string} contentType
|
|
209
|
+
* @param {boolean|string|string[]} includeRefs
|
|
210
|
+
* @return {Promise<string[]>}
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
async function _resolveIncludeRefs(contentType, includeRefs) {
|
|
214
|
+
if (!includeRefs) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof includeRefs === 'string') {
|
|
219
|
+
return [includeRefs];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (Array.isArray(includeRefs)) {
|
|
223
|
+
return includeRefs;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (includeRefs !== true) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (referenceFieldCache.has(contentType)) {
|
|
231
|
+
return referenceFieldCache.get(contentType);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const contentTypeSchema = await Stack.ContentType(contentType).fetch();
|
|
235
|
+
const referenceFields = _collectReferenceFieldPaths(contentTypeSchema.content_type.schema);
|
|
236
|
+
referenceFieldCache.set(contentType, referenceFields);
|
|
237
|
+
return referenceFields;
|
|
238
|
+
}
|
|
239
|
+
|
|
176
240
|
/**
|
|
177
241
|
* Gets all the specified entries for a multiple content type
|
|
178
242
|
* @param {string} contentType
|
|
@@ -181,19 +245,24 @@ async function getSingletonEntries(options) {
|
|
|
181
245
|
*/
|
|
182
246
|
const getMultiEntries = async (contentType, includeRefs, values) => {
|
|
183
247
|
try {
|
|
184
|
-
const
|
|
248
|
+
const resolvedIncludeRefs = await _resolveIncludeRefs(contentType, includeRefs);
|
|
249
|
+
|
|
250
|
+
let query = Stack
|
|
185
251
|
.ContentType(contentType)
|
|
186
252
|
.Query()
|
|
187
|
-
.containedIn('title', values)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.
|
|
253
|
+
.containedIn('title', values);
|
|
254
|
+
|
|
255
|
+
if (resolvedIncludeRefs.length) {
|
|
256
|
+
query = query.includeReference(resolvedIncludeRefs);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const [results] = await query.toJSON().find();
|
|
191
260
|
|
|
192
261
|
return results;
|
|
193
262
|
} catch(err) {
|
|
194
263
|
console.error(err);
|
|
195
264
|
}
|
|
196
|
-
}
|
|
265
|
+
};
|
|
197
266
|
|
|
198
267
|
/**
|
|
199
268
|
* Returns contentstack Stack object initialized with the api key, deliveryToken, and extended with custom functionality
|
|
@@ -214,8 +283,9 @@ function getStack(envrnmnt) {
|
|
|
214
283
|
}
|
|
215
284
|
|
|
216
285
|
env = envrnmnt;
|
|
217
|
-
|
|
218
|
-
|
|
286
|
+
referenceFieldCache.clear();
|
|
287
|
+
const {apiKey: api_key, deliveryToken: delivery_token, environment} = getConfig(env);
|
|
288
|
+
Stack = Contentstack.Stack({api_key, delivery_token, environment});
|
|
219
289
|
|
|
220
290
|
// extend the Stack with a custom function to get translations
|
|
221
291
|
Stack.getSingletonEntries = getSingletonEntries;
|