@nuskin/contentstack-lib 2.0.0 → 2.0.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/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,76 @@ 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: () => ({
68
+ Query: () => mockQuery,
69
+ fetch: mockFetch
57
70
  })
58
71
  }));
59
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
+
60
109
  jest.mock("contentstack", () => ({Stack: mockStack}));
61
110
  });
62
111
 
63
112
  afterEach(() => {
64
113
  mockSinglePromiseResult.mockReset();
65
114
  mockPromiseResult.mockReset();
115
+ mockContainedIn.mockReset();
116
+ mockIncludeReference.mockReset();
117
+ mockFetch.mockReset();
66
118
  //jest.resetAllMocks(); don't reset the mockStack function
67
119
  });
68
120
 
@@ -71,7 +123,7 @@ const mockResults = () => {
71
123
  mockPromiseResult.mockResolvedValueOnce(checkoutAdrStrings);
72
124
  mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutAdrStrings);
73
125
  mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutCartStrings);
74
- }
126
+ };
75
127
 
76
128
  describe("ContentstackApi", () => {
77
129
  test("getSingletonEntries", async () => {
@@ -133,8 +185,67 @@ describe("ContentstackApi", () => {
133
185
  global.window = Object.create(window);
134
186
  global.window.location = { hostname };
135
187
  const {getStack} = require('../src/api');
136
- getStack(env[index]);
188
+ getStack();
137
189
  expect(mockStack).toHaveBeenCalledWith({api_key: csCfg.apiKey, delivery_token: csCfg.deliveryToken, environment: csCfg.environment});
138
- })
139
- })
190
+ });
191
+ });
192
+
193
+ test('getSingletonEntries maps language code in to id', async () => {
194
+ mockSinglePromiseResult.mockResolvedValueOnce(singleCheckoutAdrStrings);
195
+ const {getStack} = require('../src/api');
196
+ const Stack = getStack('dev');
197
+ await Stack.getSingletonEntries({contentTypeUIDs: ['checkout_adr_strings'], language: 'in'});
198
+ expect(mockSinglePromiseResult).toHaveBeenCalled();
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
+ });
140
251
  });
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.1](https://code.tls.nuskin.io/ns-am/utility/npm/contentstack-lib/compare/v2.0.0...v2.0.1) (2026-05-22)
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.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,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,10 +174,100 @@ 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
+
240
+ /**
241
+ * Gets all the specified entries for a multiple content type
242
+ * @param {string} contentType
243
+ * @param {string[]} values
244
+ * @return {Promise<{Object}>} config
245
+ */
246
+ const getMultiEntries = async (contentType, includeRefs, values) => {
247
+ try {
248
+ const resolvedIncludeRefs = await _resolveIncludeRefs(contentType, includeRefs);
249
+
250
+ let query = Stack
251
+ .ContentType(contentType)
252
+ .Query()
253
+ .containedIn('title', values);
254
+
255
+ if (resolvedIncludeRefs.length) {
256
+ query = query.includeReference(resolvedIncludeRefs);
257
+ }
258
+
259
+ const [results] = await query.toJSON().find();
260
+
261
+ return results;
262
+ } catch(err) {
263
+ console.error(err);
264
+ }
265
+ };
266
+
176
267
  /**
177
268
  * Returns contentstack Stack object initialized with the api key, deliveryToken, and extended with custom functionality
178
269
  * @param {'dev' | 'test' | 'stage' | 'prod'} envrnmnt
179
- * @return {Object} Stack - contentstack Stack sdk object with additional getStrings function
270
+ * @return {Object} Stack - contentstack Stack sdk object with additional helper functions
180
271
  */
181
272
  function getStack(envrnmnt) {
182
273
  if (!envrnmnt) {
@@ -192,11 +283,13 @@ function getStack(envrnmnt) {
192
283
  }
193
284
 
194
285
  env = envrnmnt;
195
- const {apiKey: api_key, deliveryToken: delivery_token, environment} = getConfig(env)
196
- Stack = Contentstack.Stack({api_key, delivery_token, environment})
286
+ referenceFieldCache.clear();
287
+ const {apiKey: api_key, deliveryToken: delivery_token, environment} = getConfig(env);
288
+ Stack = Contentstack.Stack({api_key, delivery_token, environment});
197
289
 
198
290
  // extend the Stack with a custom function to get translations
199
291
  Stack.getSingletonEntries = getSingletonEntries;
292
+ Stack.getMultiEntries = getMultiEntries;
200
293
 
201
294
  return Stack;
202
295
  }
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
  };