Skip to content

Commit cdf8056

Browse files
committed
feat: add @adobe/spacecat-shared-drs-client package (LLMO-1819)
Shared HTTP client for the Data Retrieval Service (DRS) API, enabling both spacecat-api-service and spacecat-audit-worker to interact with DRS for prompt generation and brand presence re-analysis. Methods: createFrom(), isConfigured(), submitJob(), submitPromptGenerationJob(), triggerBrandDetection(), getJob() 17 unit tests, 100% coverage. Made-with: Cursor
1 parent 93ef065 commit cdf8056

File tree

9 files changed

+572
-0
lines changed

9 files changed

+572
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"reporterEnabled": "spec,xunit",
3+
"xunitReporterOptions": {
4+
"output": "junit/test-results.xml"
5+
}
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
coverage/
2+
node_modules/
3+
junit/
4+
test/
5+
docs/
6+
logs/
7+
test-results.xml
8+
renovate.json
9+
.*
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"reporter": [
3+
"lcov",
4+
"text"
5+
],
6+
"check-coverage": true,
7+
"lines": 100,
8+
"branches": 100,
9+
"statements": 100,
10+
"all": true,
11+
"include": [
12+
"src/**/*.js"
13+
]
14+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
extends: "semantic-release-monorepo",
3+
plugins: [
4+
"@semantic-release/commit-analyzer",
5+
"@semantic-release/release-notes-generator",
6+
["@semantic-release/changelog", {
7+
"changelogFile": "CHANGELOG.md",
8+
}],
9+
"@semantic-release/npm",
10+
["@semantic-release/git", {
11+
"assets": ["package.json", "CHANGELOG.md"],
12+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
13+
}],
14+
["@semantic-release/github", {}],
15+
],
16+
branches: ['main'],
17+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@adobe/spacecat-shared-drs-client",
3+
"version": "1.0.0",
4+
"description": "Shared modules of the Spacecat Services - Data Retrieval Service Client",
5+
"type": "module",
6+
"engines": {
7+
"node": ">=22.0.0 <25.0.0",
8+
"npm": ">=10.9.0 <12.0.0"
9+
},
10+
"main": "src/index.js",
11+
"types": "src/index.d.ts",
12+
"scripts": {
13+
"test": "c8 mocha 'test/**/*.test.js'",
14+
"lint": "eslint .",
15+
"clean": "rm -rf package-lock.json node_modules"
16+
},
17+
"mocha": {
18+
"require": "test/setup-env.js",
19+
"reporter": "mocha-multi-reporters",
20+
"reporter-options": "configFile=.mocha-multi.json",
21+
"spec": "test/*.test.js"
22+
},
23+
"repository": {
24+
"type": "git",
25+
"url": "https://github.com/adobe/spacecat-shared.git"
26+
},
27+
"author": "",
28+
"license": "Apache-2.0",
29+
"publishConfig": {
30+
"access": "public"
31+
},
32+
"dependencies": {
33+
"@adobe/spacecat-shared-utils": "1.98.1"
34+
},
35+
"devDependencies": {
36+
"chai": "6.2.1",
37+
"chai-as-promised": "8.0.2",
38+
"mocha": "11.7.5",
39+
"mocha-multi-reporters": "1.5.1",
40+
"nock": "14.0.10",
41+
"sinon": "21.0.1",
42+
"sinon-chai": "4.0.1",
43+
"typescript": "5.9.3"
44+
}
45+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2026 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+
interface DrsClientConfig {
14+
apiBaseUrl: string;
15+
apiKey: string;
16+
}
17+
18+
interface PromptGenerationParams {
19+
baseUrl: string;
20+
brandName: string;
21+
audience: string;
22+
region?: string;
23+
numPrompts?: number;
24+
source?: string;
25+
siteId: string;
26+
imsOrgId: string;
27+
}
28+
29+
interface BrandDetectionOptions {
30+
batchId?: string;
31+
priority?: string;
32+
}
33+
34+
interface DrsJobResult {
35+
job_id: string;
36+
[key: string]: unknown;
37+
}
38+
39+
declare class DrsClient {
40+
static createFrom(context: { env: Record<string, string>; log?: Console }): DrsClient;
41+
constructor(config: DrsClientConfig, log?: Console);
42+
isConfigured(): boolean;
43+
submitJob(params: Record<string, unknown>): Promise<DrsJobResult>;
44+
submitPromptGenerationJob(params: PromptGenerationParams): Promise<DrsJobResult>;
45+
triggerBrandDetection(siteId: string, options?: BrandDetectionOptions): Promise<unknown>;
46+
getJob(jobId: string): Promise<Record<string, unknown>>;
47+
}
48+
49+
export default DrsClient;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2026 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 { hasText, tracingFetch as fetch } from '@adobe/spacecat-shared-utils';
14+
15+
export default class DrsClient {
16+
/**
17+
* Creates a DrsClient from a universal context object.
18+
* @param {object} context - Context with env and log
19+
* @returns {DrsClient}
20+
*/
21+
static createFrom(context) {
22+
const { env, log = console } = context;
23+
const { DRS_API_URL: apiBaseUrl, DRS_API_KEY: apiKey } = env;
24+
25+
if (context.drsClient) return context.drsClient;
26+
27+
return new DrsClient({ apiBaseUrl, apiKey }, log);
28+
}
29+
30+
constructor({ apiBaseUrl, apiKey }, log = console) {
31+
this.apiBaseUrl = apiBaseUrl?.replace(/\/+$/, '');
32+
this.apiKey = apiKey;
33+
this.log = log;
34+
}
35+
36+
/**
37+
* @returns {boolean} True if DRS_API_URL and DRS_API_KEY are set
38+
*/
39+
isConfigured() {
40+
return hasText(this.apiBaseUrl) && hasText(this.apiKey);
41+
}
42+
43+
async #request(method, path, body = undefined) {
44+
if (!this.isConfigured()) {
45+
throw new Error('DRS client is not configured. Set DRS_API_URL and DRS_API_KEY environment variables.');
46+
}
47+
48+
const url = `${this.apiBaseUrl}${path}`;
49+
const options = {
50+
method,
51+
headers: {
52+
'Content-Type': 'application/json',
53+
'x-api-key': this.apiKey,
54+
},
55+
};
56+
57+
if (body) {
58+
options.body = JSON.stringify(body);
59+
}
60+
61+
const response = await fetch(url, options);
62+
63+
if (!response.ok) {
64+
const errorText = await response.text();
65+
throw new Error(`DRS ${method} ${path} failed: ${response.status} - ${errorText}`);
66+
}
67+
68+
const contentType = response.headers.get('content-type') || '';
69+
if (contentType.includes('application/json')) {
70+
return response.json();
71+
}
72+
return null;
73+
}
74+
75+
/**
76+
* Submits a generic job to DRS.
77+
* @param {object} params - Job parameters (provider_id, source, parameters, etc.)
78+
* @returns {Promise<object>} Job submission result with job_id
79+
*/
80+
async submitJob(params) {
81+
this.log.info('Submitting DRS job', { providerId: params.provider_id });
82+
const result = await this.#request('POST', '/jobs', params);
83+
this.log.info(`DRS job submitted: ${result.job_id}`, { jobId: result.job_id });
84+
return result;
85+
}
86+
87+
/**
88+
* Submits a prompt generation job to DRS.
89+
* @param {object} params
90+
* @param {string} params.baseUrl - Site base URL
91+
* @param {string} params.brandName - Brand name
92+
* @param {string} params.audience - Target audience
93+
* @param {string} [params.region='US'] - Region
94+
* @param {number} [params.numPrompts=42] - Number of prompts
95+
* @param {string} [params.source='onboarding'] - Job source
96+
* @param {string} params.siteId - SpaceCat site ID
97+
* @param {string} params.imsOrgId - IMS organization ID
98+
* @returns {Promise<object>} Job result with job_id
99+
*/
100+
async submitPromptGenerationJob({
101+
baseUrl,
102+
brandName,
103+
audience,
104+
region = 'US',
105+
numPrompts = 42,
106+
source = 'onboarding',
107+
siteId,
108+
imsOrgId,
109+
}) {
110+
this.log.info(`Submitting DRS prompt generation job for site ${siteId}`, {
111+
baseUrl, brandName, region, numPrompts,
112+
});
113+
114+
return this.submitJob({
115+
provider_id: 'prompt_generation_base_url',
116+
source,
117+
parameters: {
118+
base_url: baseUrl,
119+
brand_name: brandName,
120+
audience,
121+
region,
122+
num_prompts: numPrompts,
123+
model: 'gpt-5-nano',
124+
metadata: {
125+
site_id: siteId,
126+
imsOrgId,
127+
base_url: baseUrl,
128+
brand: brandName,
129+
region,
130+
},
131+
},
132+
});
133+
}
134+
135+
/**
136+
* Triggers brand detection re-analysis on existing data for a site.
137+
* @param {string} siteId - SpaceCat site ID
138+
* @param {object} [options={}]
139+
* @param {string} [options.batchId] - Specific batch to re-analyze
140+
* @param {string} [options.priority] - Job priority (HIGH, NORMAL)
141+
* @returns {Promise<object>} Trigger result
142+
*/
143+
async triggerBrandDetection(siteId, options = {}) {
144+
this.log.info(`Triggering DRS brand detection for site ${siteId}`, options);
145+
return this.#request('POST', `/sites/${siteId}/brand-detection`, options);
146+
}
147+
148+
/**
149+
* Gets job status and details.
150+
* @param {string} jobId - DRS job ID
151+
* @returns {Promise<object>} Job details
152+
*/
153+
async getJob(jobId) {
154+
return this.#request('GET', `/jobs/${jobId}`);
155+
}
156+
}

0 commit comments

Comments
 (0)