@adobe/spacecat-shared-utils 1.51.1 → 1.53.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-utils-v1.53.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.52.0...@adobe/spacecat-shared-utils-v1.53.0) (2025-10-02)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add regions to categories and related validation on dependent entities ([#996](https://github.com/adobe/spacecat-shared/issues/996)) ([60c52e2](https://github.com/adobe/spacecat-shared/commit/60c52e2d9c7e79a67132ae2b26e40e617e8af358))
7
+
8
+ # [@adobe/spacecat-shared-utils-v1.52.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.51.1...@adobe/spacecat-shared-utils-v1.52.0) (2025-09-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * llmo config types and version ([#990](https://github.com/adobe/spacecat-shared/issues/990)) ([2dfe331](https://github.com/adobe/spacecat-shared/commit/2dfe33156754f044b6e9ff18d32e13baabaa47f1))
14
+
1
15
  # [@adobe/spacecat-shared-utils-v1.51.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.51.0...@adobe/spacecat-shared-utils-v1.51.1) (2025-09-25)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.51.1",
3
+ "version": "1.53.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "engines": {
package/src/index.d.ts CHANGED
@@ -262,3 +262,6 @@ export function tracingFetch(url: string | Request, options?: RequestOptions): P
262
262
  export const SPACECAT_USER_AGENT: string;
263
263
 
264
264
  export function retrievePageAuthentication(site: object, context: object): Promise<string>;
265
+
266
+ export * as llmoConfig from './llmo-config.js';
267
+ export * as schemas from './schemas.js';
@@ -60,8 +60,8 @@ export function defaultConfig() {
60
60
  * @param {string} [options.version] Optional version ID of the configuration to read.
61
61
  * Defaults to the latest version.
62
62
  * @param {string} [options.s3Bucket] Optional S3 bucket name.
63
- * @returns {Promise<{config: LLMOConfig, exists: boolean}>} The configuration object and
64
- * a flag indicating if it existed.
63
+ * @returns {Promise<{config: LLMOConfig, exists: boolean, version?: string}>} The configuration,
64
+ * a flag indicating if it existed, and the version ID if it exists.
65
65
  * @throws {Error} If reading the configuration fails for reasons other than it not existing.
66
66
  */
67
67
  export async function readConfig(sideId, s3Client, options) {
@@ -79,7 +79,7 @@ export async function readConfig(sideId, s3Client, options) {
79
79
  } catch (e) {
80
80
  if (e.name === 'NoSuchKey' || e.name === 'NotFound') {
81
81
  // Config does not exist yet. Return empty config.
82
- return { config: defaultConfig(), exists: false };
82
+ return { config: defaultConfig(), exists: false, version: undefined };
83
83
  }
84
84
  throw e;
85
85
  }
@@ -90,7 +90,7 @@ export async function readConfig(sideId, s3Client, options) {
90
90
  }
91
91
  const text = await body.transformToString();
92
92
  const config = llmoConfig.parse(JSON.parse(text));
93
- return { config, exists: true };
93
+ return { config, exists: true, version: res.VersionId || undefined };
94
94
  }
95
95
 
96
96
  /**
package/src/schemas.js CHANGED
@@ -36,14 +36,14 @@ import * as z from 'zod';
36
36
 
37
37
  const nonEmptyString = z.string().min(1);
38
38
 
39
+ const region = z.string().length(2).regex(/^[a-z][a-z]$/i);
40
+
39
41
  const entity = z.union([
40
- z.object({ type: z.literal('category'), name: nonEmptyString }),
42
+ z.object({ type: z.literal('category'), name: nonEmptyString, region: z.union([region, z.array(region)]).optional() }),
41
43
  z.object({ type: z.literal('topic'), name: nonEmptyString }),
42
44
  z.object({ type: nonEmptyString }),
43
45
  ]);
44
46
 
45
- const region = z.string().length(2).regex(/^[a-z][a-z]$/i);
46
-
47
47
  export const llmoConfig = z.object({
48
48
  entities: z.record(z.uuid(), entity),
49
49
  brands: z.object({
@@ -73,10 +73,12 @@ export const llmoConfig = z.object({
73
73
  brands.aliases.forEach((alias, index) => {
74
74
  ensureEntityType(entities, ctx, alias.category, 'category', ['brands', 'aliases', index, 'category'], 'category');
75
75
  ensureEntityType(entities, ctx, alias.topic, 'topic', ['brands', 'aliases', index, 'topic'], 'topic');
76
+ ensureRegionCompatibility(entities, ctx, alias.category, alias.region, ['brands', 'aliases', index, 'region'], 'brand alias');
76
77
  });
77
78
 
78
79
  competitors.competitors.forEach((competitor, index) => {
79
80
  ensureEntityType(entities, ctx, competitor.category, 'category', ['competitors', 'competitors', index, 'category'], 'category');
81
+ ensureRegionCompatibility(entities, ctx, competitor.category, competitor.region, ['competitors', 'competitors', index, 'region'], 'competitor');
80
82
  });
81
83
  });
82
84
 
@@ -107,3 +109,47 @@ function ensureEntityType(entities, ctx, id, expectedType, path, refLabel) {
107
109
  });
108
110
  }
109
111
  }
112
+
113
+ /**
114
+ * @param {LLMOConfig['entities']} entities
115
+ * @param {z.RefinementCtx} ctx
116
+ * @param {string} categoryId
117
+ * @param {string | string[]} itemRegion
118
+ * @param {Array<number | string>} path
119
+ * @param {string} itemLabel
120
+ */
121
+ function ensureRegionCompatibility(entities, ctx, categoryId, itemRegion, path, itemLabel) {
122
+ const categoryEntity = entities[categoryId];
123
+ if (!categoryEntity) {
124
+ // Category validation is handled by ensureEntityType
125
+ return;
126
+ }
127
+
128
+ const categoryRegions = categoryEntity.region;
129
+
130
+ // If category has no regions defined, item should not have regions
131
+ if (!categoryRegions) {
132
+ ctx.addIssue({
133
+ code: 'custom',
134
+ path,
135
+ message: `${itemLabel} cannot have regions when the referenced category has no regions defined`,
136
+ });
137
+ return;
138
+ }
139
+
140
+ // Normalize regions to arrays for comparison
141
+ const categoryRegionArray = Array.isArray(categoryRegions) ? categoryRegions : [categoryRegions];
142
+ const itemRegionArray = Array.isArray(itemRegion) ? itemRegion : [itemRegion];
143
+
144
+ // Check if all item regions are contained in category regions
145
+ const invalidRegions = itemRegionArray.filter(
146
+ (regionItem) => !categoryRegionArray.includes(regionItem),
147
+ );
148
+ if (invalidRegions.length > 0) {
149
+ ctx.addIssue({
150
+ code: 'custom',
151
+ path,
152
+ message: `${itemLabel} regions [${invalidRegions.join(', ')}] are not allowed. Category only supports regions: [${categoryRegionArray.join(', ')}]`,
153
+ });
154
+ }
155
+ }