@nuskin/contentstack-lib 2.0.1 → 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/.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",
@@ -64,46 +64,120 @@ beforeEach(() => {
64
64
  mockContainedIn.mockImplementation(() => mockQuery);
65
65
  mockIncludeReference.mockImplementation(() => mockQuery);
66
66
  mockStack.mockImplementation(() => ({
67
- ContentType: () => ({
67
+ ContentType: (contentType) => ({
68
68
  Query: () => mockQuery,
69
- fetch: mockFetch
69
+ fetch: () => mockFetch(contentType)
70
70
  })
71
71
  }));
72
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',
73
+ mockFetch.mockImplementation((contentType) => {
74
+ const schemas = {
75
+ shipping_address_form: {
76
+ content_type: {
83
77
  schema: [
84
78
  {
85
- uid: 'address_form',
86
- data_type: 'reference'
87
- }
88
- ]
89
- },
90
- {
91
- uid: 'promo_blocks',
92
- data_type: 'blocks',
93
- blocks: [
79
+ uid: 'countries',
80
+ data_type: 'reference',
81
+ reference_to: 'market'
82
+ },
94
83
  {
95
- uid: 'hero',
84
+ uid: 'details',
85
+ data_type: 'group',
96
86
  schema: [
97
87
  {
98
- uid: 'banner',
99
- data_type: 'reference'
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
+ ]
100
107
  }
101
108
  ]
102
109
  }
103
110
  ]
104
111
  }
105
- ]
106
- }
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: []}});
107
181
  });
108
182
 
109
183
  jest.mock("contentstack", () => ({Stack: mockStack}));
@@ -222,10 +296,13 @@ describe("ContentstackApi", () => {
222
296
  const {getStack} = require('../src/api');
223
297
  const Stack = getStack('dev');
224
298
  await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
225
- expect(mockFetch).toHaveBeenCalledTimes(1);
299
+ expect(mockFetch).toHaveBeenCalledTimes(6);
226
300
  expect(mockIncludeReference).toHaveBeenCalledWith([
227
301
  'countries',
228
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',
229
306
  'promo_blocks.hero.banner'
230
307
  ]);
231
308
  });
@@ -236,16 +313,41 @@ describe("ContentstackApi", () => {
236
313
  const Stack = getStack('test');
237
314
  await Stack.getMultiEntries('shipping_address_form', true, ['United States']);
238
315
  await Stack.getMultiEntries('shipping_address_form', true, ['Canada']);
239
- expect(mockFetch).toHaveBeenCalledTimes(1);
316
+ expect(mockFetch).toHaveBeenCalledTimes(6);
240
317
  expect(mockIncludeReference).toHaveBeenNthCalledWith(1, [
241
318
  'countries',
242
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',
243
323
  'promo_blocks.hero.banner'
244
324
  ]);
245
325
  expect(mockIncludeReference).toHaveBeenNthCalledWith(2, [
246
326
  'countries',
247
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',
248
346
  'promo_blocks.hero.banner'
249
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
+ ]));
250
352
  });
251
353
  });
package/docs/CHANGELOG.md CHANGED
@@ -1 +1 @@
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)
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.1",
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": {
package/src/api.js CHANGED
@@ -9,6 +9,8 @@ const {contentstack: prodEnvConfig} = require(`../config/prod.json`);
9
9
  let env = null;
10
10
  let Stack = null;
11
11
  const referenceFieldCache = new Map();
12
+ const contentTypeSchemaCache = new Map();
13
+ const MAX_REFERENCE_DEPTH = 4;
12
14
 
13
15
  const COMMON_LANGUAGES = {
14
16
  'da': 'da-dk',
@@ -174,33 +176,80 @@ async function getSingletonEntries(options) {
174
176
  }
175
177
  }
176
178
 
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) => {
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) {
186
194
  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
- });
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);
204
253
  }
205
254
 
206
255
  /**
@@ -231,8 +280,7 @@ async function _resolveIncludeRefs(contentType, includeRefs) {
231
280
  return referenceFieldCache.get(contentType);
232
281
  }
233
282
 
234
- const contentTypeSchema = await Stack.ContentType(contentType).fetch();
235
- const referenceFields = _collectReferenceFieldPaths(contentTypeSchema.content_type.schema);
283
+ const referenceFields = await _collectReferenceFieldPathsForContentType(contentType);
236
284
  referenceFieldCache.set(contentType, referenceFields);
237
285
  return referenceFields;
238
286
  }
@@ -284,6 +332,7 @@ function getStack(envrnmnt) {
284
332
 
285
333
  env = envrnmnt;
286
334
  referenceFieldCache.clear();
335
+ contentTypeSchemaCache.clear();
287
336
  const {apiKey: api_key, deliveryToken: delivery_token, environment} = getConfig(env);
288
337
  Stack = Contentstack.Stack({api_key, delivery_token, environment});
289
338