@adobe/spacecat-shared-utils 1.50.7 → 1.51.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 +14 -0
- package/package.json +6 -5
- package/src/index.js +3 -0
- package/src/llmo-config.js +119 -0
- package/src/schemas.js +109 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-utils-v1.51.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.50.8...@adobe/spacecat-shared-utils-v1.51.0) (2025-09-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* schema + file read/write for llmo config ([#981](https://github.com/adobe/spacecat-shared/issues/981)) ([93b0aae](https://github.com/adobe/spacecat-shared/commit/93b0aae0daeb1a81c6914d21b30f39b448784599))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-utils-v1.50.8](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.50.7...@adobe/spacecat-shared-utils-v1.50.8) (2025-09-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **deps:** update external fixes ([#969](https://github.com/adobe/spacecat-shared/issues/969)) ([d722c62](https://github.com/adobe/spacecat-shared/commit/d722c623193fdbf292d96d71236cb4396db7ce3b))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-utils-v1.50.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.50.6...@adobe/spacecat-shared-utils-v1.50.7) (2025-09-18)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.0",
|
|
4
4
|
"description": "Shared modules of the Spacecat Services - utils",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,12 +48,13 @@
|
|
|
48
48
|
"@adobe/fetch": "4.2.3",
|
|
49
49
|
"@adobe/spacecat-shared-data-access": "2.45.0",
|
|
50
50
|
"@adobe/spacecat-shared-ims-client": "1.8.3",
|
|
51
|
-
"@aws-sdk/client-s3": "3.
|
|
52
|
-
"@aws-sdk/client-secrets-manager": "3.
|
|
53
|
-
"@aws-sdk/client-sqs": "3.
|
|
51
|
+
"@aws-sdk/client-s3": "3.893.0",
|
|
52
|
+
"@aws-sdk/client-secrets-manager": "3.893.0",
|
|
53
|
+
"@aws-sdk/client-sqs": "3.893.0",
|
|
54
54
|
"@json2csv/plainjs": "7.0.6",
|
|
55
55
|
"aws-xray-sdk": "3.10.3",
|
|
56
56
|
"date-fns": "4.1.0",
|
|
57
|
-
"validator": "^13.15.15"
|
|
57
|
+
"validator": "^13.15.15",
|
|
58
|
+
"zod": "^4.1.11"
|
|
58
59
|
}
|
|
59
60
|
}
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
14
|
+
import { llmoConfig } from './schemas.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @import { S3Client } from "@aws-sdk/client-s3"
|
|
18
|
+
* @import { LLMOConfig } from "./schemas.js"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} siteId The ID of the site to get the config directory for.
|
|
23
|
+
* @returns {string} The configuration directory path for the given site ID.
|
|
24
|
+
*/
|
|
25
|
+
export function lmmoConfigDir(siteId) {
|
|
26
|
+
return `config/llmo/${siteId}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} siteId The ID of the site to get the latest config file path for.
|
|
31
|
+
* @returns {string} The latest configuration file path for the given site ID.
|
|
32
|
+
*/
|
|
33
|
+
export function llmoConfigPath(siteId) {
|
|
34
|
+
return `${lmmoConfigDir(siteId)}/lmmo-config.json`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the default LLMO configuration.
|
|
39
|
+
* @returns {LLMOConfig} The default configuration object.
|
|
40
|
+
*/
|
|
41
|
+
export function defaultConfig() {
|
|
42
|
+
return {
|
|
43
|
+
entities: {},
|
|
44
|
+
brands: {
|
|
45
|
+
aliases: [],
|
|
46
|
+
},
|
|
47
|
+
competitors: {
|
|
48
|
+
competitors: [],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reads the LLMO configuration for a given site.
|
|
55
|
+
* Returns an empty configuration if the configuration does not exist.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} sideId The ID of the site.
|
|
58
|
+
* @param {S3Client} s3Client The S3 client to use for reading the configuration.
|
|
59
|
+
* @param {object} [options]
|
|
60
|
+
* @param {string} [options.version] Optional version ID of the configuration to read.
|
|
61
|
+
* Defaults to the latest version.
|
|
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.
|
|
65
|
+
* @throws {Error} If reading the configuration fails for reasons other than it not existing.
|
|
66
|
+
*/
|
|
67
|
+
export async function readConfig(sideId, s3Client, options) {
|
|
68
|
+
const version = options?.version;
|
|
69
|
+
const s3Bucket = options?.s3Bucket || process.env.S3_BUCKET_NAME;
|
|
70
|
+
|
|
71
|
+
const getObjectCommand = new GetObjectCommand({
|
|
72
|
+
Bucket: s3Bucket,
|
|
73
|
+
Key: llmoConfigPath(sideId),
|
|
74
|
+
VersionId: version,
|
|
75
|
+
});
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await s3Client.send(getObjectCommand);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e.name === 'NoSuchKey' || e.name === 'NotFound') {
|
|
81
|
+
// Config does not exist yet. Return empty config.
|
|
82
|
+
return { config: defaultConfig(), exists: false };
|
|
83
|
+
}
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const body = res.Body;
|
|
88
|
+
if (!body) {
|
|
89
|
+
throw new Error('LLMO config body is empty');
|
|
90
|
+
}
|
|
91
|
+
const text = await body.transformToString();
|
|
92
|
+
const config = llmoConfig.parse(JSON.parse(text));
|
|
93
|
+
return { config, exists: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Writes the LLMO configuration for a given site.
|
|
98
|
+
* @param {string} siteId The ID of the site.
|
|
99
|
+
* @param {LLMOConfig} config The configuration object to write.
|
|
100
|
+
* @param {S3Client} s3Client The S3 client to use for reading the configuration.
|
|
101
|
+
* @param {object} [options]
|
|
102
|
+
* @param {string} [options.s3Bucket] Optional S3 bucket name.
|
|
103
|
+
* @returns {Promise<{ version: string }>} The version of the configuration written.
|
|
104
|
+
*/
|
|
105
|
+
export async function writeConfig(siteId, config, s3Client, options) {
|
|
106
|
+
const s3Bucket = options?.s3Bucket || process.env.S3_BUCKET_NAME;
|
|
107
|
+
|
|
108
|
+
const putObjectCommand = new PutObjectCommand({
|
|
109
|
+
Bucket: s3Bucket,
|
|
110
|
+
Key: llmoConfigPath(siteId),
|
|
111
|
+
Body: JSON.stringify(config, null, 2),
|
|
112
|
+
ContentType: 'application/json',
|
|
113
|
+
});
|
|
114
|
+
const res = await s3Client.send(putObjectCommand);
|
|
115
|
+
if (!res.VersionId) {
|
|
116
|
+
throw new Error('Failed to get version ID after writing LLMO config');
|
|
117
|
+
}
|
|
118
|
+
return { version: res.VersionId };
|
|
119
|
+
}
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* eslint-disable no-use-before-define */
|
|
14
|
+
|
|
15
|
+
import * as z from 'zod';
|
|
16
|
+
|
|
17
|
+
// ===== SCHEMA DEFINITIONS ====================================================
|
|
18
|
+
// Schemas defined here must be forward- and backward-compatible when making changes.
|
|
19
|
+
// This means:
|
|
20
|
+
// - Always wrap arrays in an object, so that extra properties can be added later.
|
|
21
|
+
// - When using unions, always include a catchall case to cover unknown future cases.
|
|
22
|
+
// - Always allow extra properties in objects, so that future config versions don't break parsing.
|
|
23
|
+
// (this is the default. Do not add `.strict()`!)
|
|
24
|
+
// - it is ok to add new optional properties to objects, but not to remove existing ones.
|
|
25
|
+
// - enums (z.enum([...])) are not forward-compatible.
|
|
26
|
+
// Use z.string() or `z.union([..., z.string()])` instead.
|
|
27
|
+
// - never rename properties, only add new ones.
|
|
28
|
+
// - never broaden or narrow types.
|
|
29
|
+
// E.g., don't change `z.string()` to `z.union([z.string(), z.number()])` or vice versa.
|
|
30
|
+
// If you anticipate that need, use the union from the start.
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {z.output<llmoConfig>} LLMOConfig
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const nonEmptyString = z.string().min(1);
|
|
38
|
+
|
|
39
|
+
const entity = z.union([
|
|
40
|
+
z.object({ type: z.literal('category'), name: nonEmptyString }),
|
|
41
|
+
z.object({ type: z.literal('topic'), name: nonEmptyString }),
|
|
42
|
+
z.object({ type: nonEmptyString }),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const region = z.string().length(2).regex(/^[a-z][a-z]$/i);
|
|
46
|
+
|
|
47
|
+
export const llmoConfig = z.object({
|
|
48
|
+
entities: z.record(z.uuid(), entity),
|
|
49
|
+
brands: z.object({
|
|
50
|
+
aliases: z.array(
|
|
51
|
+
z.object({
|
|
52
|
+
aliases: z.array(nonEmptyString),
|
|
53
|
+
category: z.uuid(),
|
|
54
|
+
region: z.union([region, z.array(region)]),
|
|
55
|
+
topic: z.uuid(),
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
competitors: z.object({
|
|
60
|
+
competitors: z.array(
|
|
61
|
+
z.object({
|
|
62
|
+
category: z.uuid(),
|
|
63
|
+
region: z.union([region, z.array(region)]),
|
|
64
|
+
name: nonEmptyString,
|
|
65
|
+
aliases: z.array(nonEmptyString),
|
|
66
|
+
urls: z.array(z.url().optional()),
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
}).superRefine((value, ctx) => {
|
|
71
|
+
const { entities, brands, competitors } = value;
|
|
72
|
+
|
|
73
|
+
brands.aliases.forEach((alias, index) => {
|
|
74
|
+
ensureEntityType(entities, ctx, alias.category, 'category', ['brands', 'aliases', index, 'category'], 'category');
|
|
75
|
+
ensureEntityType(entities, ctx, alias.topic, 'topic', ['brands', 'aliases', index, 'topic'], 'topic');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
competitors.competitors.forEach((competitor, index) => {
|
|
79
|
+
ensureEntityType(entities, ctx, competitor.category, 'category', ['competitors', 'competitors', index, 'category'], 'category');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {LLMOConfig['entities']} entities
|
|
85
|
+
* @param {z.RefinementCtx} ctx
|
|
86
|
+
* @param {string} id
|
|
87
|
+
* @param {string} expectedType
|
|
88
|
+
* @param {Array<number | string>} path
|
|
89
|
+
* @param {string} refLabel
|
|
90
|
+
*/
|
|
91
|
+
function ensureEntityType(entities, ctx, id, expectedType, path, refLabel) {
|
|
92
|
+
const entityValue = entities[id];
|
|
93
|
+
if (!entityValue) {
|
|
94
|
+
ctx.addIssue({
|
|
95
|
+
code: 'custom',
|
|
96
|
+
path,
|
|
97
|
+
message: `Unknown ${refLabel} entity: ${id}`,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (entityValue.type !== expectedType) {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: 'custom',
|
|
105
|
+
path,
|
|
106
|
+
message: `Entity ${id} referenced as ${refLabel} must have type "${expectedType}" but was "${entityValue.type}"`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|