@nuskin/contentstack-lib 2.0.0 → 2.0.2-pa-1190.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/.eslintrc.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "SharedArrayBuffer": "readonly"
12
12
  },
13
13
  "parserOptions": {
14
- "ecmaVersion": 2018,
14
+ "ecmaVersion": 2020,
15
15
  "sourceType": "module"
16
16
  },
17
17
  "extends": [
package/.releaserc CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "branches": [
3
- "master"
3
+ "master", {"name":"PA-1190", "channel":"prerelease", "prerelease":"pa-1190"}
4
4
  ],
5
5
  "plugins": [
6
6
  "@semantic-release/release-notes-generator",
package/README.md CHANGED
@@ -10,13 +10,13 @@ FOR COMMON SPANISH AND MX-es FOR EXAMPLE.
10
10
 
11
11
  ## Installing
12
12
 
13
- Usng npm:
13
+ Using npm:
14
14
 
15
15
  ```bash
16
16
  npm install @nuskin/contentstack-lib
17
17
  ```
18
18
 
19
- Usng yarn:
19
+ Using yarn:
20
20
 
21
21
  ```bash
22
22
  yarn add @nuskin/contentstack-lib
@@ -27,8 +27,9 @@ yarn add @nuskin/contentstack-lib
27
27
  ```js
28
28
  const { getStack } = require('@nuskin/contentstack-lib')
29
29
 
30
- // Call getStack to get the stack object
31
- const Stack = getStack()
30
+ // Pass an environment explicitly outside the browser.
31
+ // In the browser, getStack() can infer the env from the hostname.
32
+ const Stack = getStack('prod')
32
33
 
33
34
  // get all languages
34
35
  const languages = await Stack.ContentType('supported_language').Query().toJSON().find();
@@ -36,16 +37,6 @@ const languages = await Stack.ContentType('supported_language').Query().toJSON()
36
37
  // get all markets
37
38
  const markets = await Stack.ContentType('market').Query().toJSON().find();
38
39
 
39
- // Use the extended getStrings function to get translations
40
- // ==================== Deprecated - Dont use this function ==========================
41
- const translations = Stack.getStrings({
42
- contentTypes: ['checkout_adr_strings', 'checkout_cart_strings'], // List of content types you want to get translations for
43
- language: 'en',
44
- country: 'US',
45
- merge: true // Will combine checkout_adr_strings and checkout_cart_strings object with single list of strings
46
- });
47
-
48
- // Use this function instead to get your translations
49
40
  // The entries are returned in the order they were requested so you can destructure the array accordingly
50
41
  // To get the result in camelcase instead of snake case you can pass in the option.camelcase: true
51
42
  // Contentstack does not merge common english strings with en_US strings. This function allows you to merge the two with mergeWithFallback: true
@@ -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,27 +45,150 @@ function getConfig(env) {
42
45
  }
43
46
 
44
47
  beforeEach(() => {
45
- mockStack.mockImplementation(() => ({
46
- ContentType: () => ({
47
- Query: () => ({
48
- language: () => ({
49
- includeFallback: () => ({
50
- toJSON: () => ({
51
- find: mockPromiseResult,
52
- findOne: mockSinglePromiseResult
53
- })
54
- })
48
+ const mockQuery = {
49
+ language: jest.fn(() => ({
50
+ includeFallback: () => ({
51
+ toJSON: () => ({
52
+ find: mockPromiseResult,
53
+ findOne: mockSinglePromiseResult
55
54
  })
56
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: (contentType) => ({
68
+ Query: () => mockQuery,
69
+ fetch: () => mockFetch(contentType)
57
70
  })
58
71
  }));
59
72
 
73
+ mockFetch.mockImplementation((contentType) => {
74
+ const schemas = {
75
+ shipping_address_form: {
76
+ content_type: {
77
+ schema: [
78
+ {
79
+ uid: 'countries',
80
+ data_type: 'reference',
81
+ reference_to: 'market'
82
+ },
83
+ {
84
+ uid: 'details',
85
+ data_type: 'group',
86
+ schema: [
87
+ {
88
+ uid: 'address_form',
89
+ data_type: 'reference',
90
+ reference_to: 'address_form'
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ uid: 'promo_blocks',
96
+ data_type: 'blocks',
97
+ blocks: [
98
+ {
99
+ uid: 'hero',
100
+ schema: [
101
+ {
102
+ uid: 'banner',
103
+ data_type: 'reference',
104
+ reference_to: 'banner'
105
+ }
106
+ ]
107
+ }
108
+ ]
109
+ }
110
+ ]
111
+ }
112
+ },
113
+ address_form: {
114
+ content_type: {
115
+ schema: [
116
+ {
117
+ uid: 'address_elements',
118
+ data_type: 'reference',
119
+ reference_to: 'address_form_field'
120
+ }
121
+ ]
122
+ }
123
+ },
124
+ market: {
125
+ content_type: {
126
+ schema: []
127
+ }
128
+ },
129
+ banner: {
130
+ content_type: {
131
+ schema: []
132
+ }
133
+ },
134
+ address_form_field: {
135
+ content_type: {
136
+ schema: [
137
+ {
138
+ uid: 'field_config',
139
+ data_type: 'reference',
140
+ reference_to: 'field_config'
141
+ }
142
+ ]
143
+ }
144
+ },
145
+ field_config: {
146
+ content_type: {
147
+ schema: [
148
+ {
149
+ uid: 'validation_rule',
150
+ data_type: 'reference',
151
+ reference_to: 'validation_rule'
152
+ }
153
+ ]
154
+ }
155
+ },
156
+ validation_rule: {
157
+ content_type: {
158
+ schema: [
159
+ {
160
+ uid: 'too_deep_reference',
161
+ data_type: 'reference',
162
+ reference_to: 'too_deep_reference'
163
+ }
164
+ ]
165
+ }
166
+ },
167
+ too_deep_reference: {
168
+ content_type: {
169
+ schema: [
170
+ {
171
+ uid: 'should_not_be_included',
172
+ data_type: 'reference',
173
+ reference_to: 'market'
174
+ }
175
+ ]
176
+ }
177
+ }
178
+ };
179
+
180
+ return Promise.resolve(schemas[contentType] || {content_type: {schema: []}});
181
+ });
182
+
60
183
  jest.mock("contentstack", () => ({Stack: mockStack}));
61
184
  });
62
185
 
63
186
  afterEach(() => {
64
187
  mockSinglePromiseResult.mockReset();
65
188
  mockPromiseResult.mockReset();
189
+ mockContainedIn.mockReset();
190
+ mockIncludeReference.mockReset();
191
+ mockFetch.mockReset();
66
192
  //jest.resetAllMocks(); don't reset the mockStack function
67
193
  });
68
194
 
@@ -71,7 +197,7 @@ const mockResults = () => {
71
197
  mockPromiseResult.mockResolvedValueOnce(checkoutAdrStrings);
72
198
  mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutAdrStrings);
73
199
  mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutCartStrings);
74
- }
200
+ };
75
201
 
76
202
  describe("ContentstackApi", () => {
77
203
  test("getSingletonEntries", async () => {
@@ -133,8 +259,95 @@ describe("ContentstackApi", () => {
133
259
  global.window = Object.create(window);
134
260
  global.window.location = { hostname };
135
261
  const {getStack} = require('../src/api');
136
- getStack(env[index]);
262
+ getStack();
137
263
  expect(mockStack).toHaveBeenCalledWith({api_key: csCfg.apiKey, delivery_token: csCfg.deliveryToken, environment: csCfg.environment});
138
- })
139
- })
264
+ });
265
+ });
266
+
267
+ test('getSingletonEntries maps language code in to id', async () => {
268
+ mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutAdrStrings);
269
+ const {getStack} = require('../src/api');
270
+ const Stack = getStack('dev');
271
+ await Stack.getSingletonEntries({contentTypeUIDs: ['checkout_adr_strings'], language: 'in'});
272
+ expect(mockSinglePromiseResult).toHaveBeenCalled();
273
+ });
274
+
275
+ test('getMultiEntries does not include references when includeRefs is false', async () => {
276
+ mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
277
+ const {getStack} = require('../src/api');
278
+ const Stack = getStack('dev');
279
+ await Stack.getMultiEntries('market', false, ['United States']);
280
+ expect(mockContainedIn).toHaveBeenCalledWith('title', ['United States']);
281
+ expect(mockIncludeReference).not.toHaveBeenCalled();
282
+ expect(mockFetch).not.toHaveBeenCalled();
283
+ });
284
+
285
+ test('getMultiEntries includes explicit reference paths', async () => {
286
+ mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
287
+ const {getStack} = require('../src/api');
288
+ const Stack = getStack('dev');
289
+ await Stack.getMultiEntries('market', ['countries', 'address_form.address_elements'], ['United States']);
290
+ expect(mockIncludeReference).toHaveBeenCalledWith(['countries', 'address_form.address_elements']);
291
+ expect(mockFetch).not.toHaveBeenCalled();
292
+ });
293
+
294
+ test('getMultiEntries discovers reference paths from schema when includeRefs is true', async () => {
295
+ mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
296
+ const {getStack} = require('../src/api');
297
+ const Stack = getStack('dev');
298
+ await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
299
+ expect(mockFetch).toHaveBeenCalledTimes(6);
300
+ expect(mockIncludeReference).toHaveBeenCalledWith([
301
+ 'countries',
302
+ 'details.address_form',
303
+ 'details.address_form.address_elements',
304
+ 'details.address_form.address_elements.field_config',
305
+ 'details.address_form.address_elements.field_config.validation_rule',
306
+ 'promo_blocks.hero.banner'
307
+ ]);
308
+ });
309
+
310
+ test('getMultiEntries caches discovered reference paths per content type', async () => {
311
+ mockPromiseResult.mockResolvedValue([[{title: 'United States'}]]);
312
+ const {getStack} = require('../src/api');
313
+ const Stack = getStack('test');
314
+ await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
315
+ await Stack.getMultiEntries('shipping_address_form', true, ['Canada']);
316
+ expect(mockFetch).toHaveBeenCalledTimes(6);
317
+ expect(mockIncludeReference).toHaveBeenNthCalledWith(1, [
318
+ 'countries',
319
+ 'details.address_form',
320
+ 'details.address_form.address_elements',
321
+ 'details.address_form.address_elements.field_config',
322
+ 'details.address_form.address_elements.field_config.validation_rule',
323
+ 'promo_blocks.hero.banner'
324
+ ]);
325
+ expect(mockIncludeReference).toHaveBeenNthCalledWith(2, [
326
+ 'countries',
327
+ 'details.address_form',
328
+ 'details.address_form.address_elements',
329
+ 'details.address_form.address_elements.field_config',
330
+ 'details.address_form.address_elements.field_config.validation_rule',
331
+ 'promo_blocks.hero.banner'
332
+ ]);
333
+ });
334
+
335
+ test('getMultiEntries limits automatic reference traversal to four levels deep', async () => {
336
+ mockPromiseResult.mockResolvedValueOnce([[{title: 'United States'}]]);
337
+ const {getStack} = require('../src/api');
338
+ const Stack = getStack('stage');
339
+ await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
340
+ expect(mockIncludeReference).toHaveBeenCalledWith([
341
+ 'countries',
342
+ 'details.address_form',
343
+ 'details.address_form.address_elements',
344
+ 'details.address_form.address_elements.field_config',
345
+ 'details.address_form.address_elements.field_config.validation_rule',
346
+ 'promo_blocks.hero.banner'
347
+ ]);
348
+ expect(mockIncludeReference).not.toHaveBeenCalledWith(expect.arrayContaining([
349
+ 'details.address_form.address_elements.field_config.validation_rule.too_deep_reference',
350
+ 'details.address_form.address_elements.field_config.validation_rule.too_deep_reference.should_not_be_included'
351
+ ]));
352
+ });
140
353
  });
package/docs/CHANGELOG.md CHANGED
@@ -1 +1 @@
1
- # [2.0.0](https://code.tls.nuskin.io/ns-am/utility/npm/contentstack-lib/compare/v1.0.0...v2.0.0) (2026-03-24)
1
+ ## [2.0.2-pa-1190.1](https://code.tls.nuskin.io/ns-am/utility/npm/contentstack-lib/compare/v2.0.1...v2.0.2-pa-1190.1) (2026-06-02)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuskin/contentstack-lib",
3
- "version": "2.0.0",
3
+ "version": "2.0.2-pa-1190.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": {
@@ -19,7 +19,7 @@
19
19
  "license": "ISC",
20
20
  "homepage": "https://code.tls.nuskin.io/ns-am/utility/npm/contentstack-lib/blob/master/README.md",
21
21
  "devDependencies": {
22
- "eslint": "5.16.0",
22
+ "eslint": "7.32.0",
23
23
  "eslint-config-google": "0.14.0",
24
24
  "eslint-config-prettier": "4.1.0",
25
25
  "eslint-plugin-json": "2.1.1",
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,10 +176,146 @@ 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
+ * Gets all the specified entries for a multiple content type
290
+ * @param {string} contentType
291
+ * @param {string[]} values
292
+ * @return {Promise<{Object}>} config
293
+ */
294
+ const getMultiEntries = async (contentType, includeRefs, values) => {
295
+ try {
296
+ const resolvedIncludeRefs = await _resolveIncludeRefs(contentType, includeRefs);
297
+
298
+ let query = Stack
299
+ .ContentType(contentType)
300
+ .Query()
301
+ .containedIn('title', values);
302
+
303
+ if (resolvedIncludeRefs.length) {
304
+ query = query.includeReference(resolvedIncludeRefs);
305
+ }
306
+
307
+ const [results] = await query.toJSON().find();
308
+
309
+ return results;
310
+ } catch(err) {
311
+ console.error(err);
312
+ }
313
+ };
314
+
176
315
  /**
177
316
  * Returns contentstack Stack object initialized with the api key, deliveryToken, and extended with custom functionality
178
317
  * @param {'dev' | 'test' | 'stage' | 'prod'} envrnmnt
179
- * @return {Object} Stack - contentstack Stack sdk object with additional getStrings function
318
+ * @return {Object} Stack - contentstack Stack sdk object with additional helper functions
180
319
  */
181
320
  function getStack(envrnmnt) {
182
321
  if (!envrnmnt) {
@@ -192,11 +331,14 @@ function getStack(envrnmnt) {
192
331
  }
193
332
 
194
333
  env = envrnmnt;
195
- const {apiKey: api_key, deliveryToken: delivery_token, environment} = getConfig(env)
196
- Stack = Contentstack.Stack({api_key, delivery_token, environment})
334
+ referenceFieldCache.clear();
335
+ contentTypeSchemaCache.clear();
336
+ const {apiKey: api_key, deliveryToken: delivery_token, environment} = getConfig(env);
337
+ Stack = Contentstack.Stack({api_key, delivery_token, environment});
197
338
 
198
339
  // extend the Stack with a custom function to get translations
199
340
  Stack.getSingletonEntries = getSingletonEntries;
341
+ Stack.getMultiEntries = getMultiEntries;
200
342
 
201
343
  return Stack;
202
344
  }
package/src/index.js CHANGED
@@ -5,9 +5,8 @@
5
5
 
6
6
  "use strict";
7
7
 
8
- const {getStack, init} = require("./api.js");
8
+ const {getStack} = require("./api.js");
9
9
 
10
10
  module.exports = {
11
- init,
12
11
  getStack
13
12
  };