@adobe/spacecat-shared-project-engine-client 1.13.0 → 1.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-project-engine-client",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Shared modules of the Spacecat Services - Semrush Project Engine client and generated types",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/errors.js ADDED
@@ -0,0 +1,48 @@
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
+ // @ts-check
14
+
15
+ /**
16
+ * The single typed error the transport facade throws from its `unwrap` seam. It carries the
17
+ * failing HTTP `method`, the response `status` (or `undefined` when there was no HTTP response —
18
+ * i.e. an exhausted-network / per-attempt-timeout failure), and the normalized parsed error
19
+ * `body`. On the network/timeout path the original thrown error is preserved as `cause`.
20
+ *
21
+ * This class does NOT translate the failure into an HTTP status for the consumer, redact the
22
+ * body, or map it onto a domain error — those are consumer-owned per ADR-0001. It exists only to
23
+ * give consumers a single `instanceof`-checkable type with the raw `{ status, method, body }`.
24
+ */
25
+ export class ProjectEngineApiError extends Error {
26
+ /**
27
+ * @param {number | undefined} status the HTTP response status, or `undefined` when there was no
28
+ * HTTP response (network / timeout failure)
29
+ * @param {string} method the HTTP method of the failing request
30
+ * @param {unknown} body the normalized parsed error body (`null` when empty/absent)
31
+ * @param {{ cause?: unknown }} [options] optional `{ cause }` forwarded to `super`, so a wrapped
32
+ * network/timeout error keeps its original as `.cause`
33
+ */
34
+ constructor(status, method, body, options) {
35
+ const message = status === undefined
36
+ ? `Project Engine ${method} request failed`
37
+ : `Project Engine ${method} request failed with status ${status}`;
38
+ super(message, options);
39
+ /** @type {string} */
40
+ this.name = 'ProjectEngineApiError';
41
+ /** @type {number | undefined} */
42
+ this.status = status;
43
+ /** @type {string} */
44
+ this.method = method;
45
+ /** @type {unknown} */
46
+ this.body = body;
47
+ }
48
+ }
package/src/index.d.ts CHANGED
@@ -353,5 +353,30 @@ export declare function createSerenityProjectEngineTransport(
353
353
  options: SerenityProjectEngineApiClientOptions,
354
354
  ): SerenityProjectEngineTransport;
355
355
 
356
+ /**
357
+ * The single typed error the transport facade throws from its `unwrap` seam. It carries the
358
+ * failing HTTP `method`, the response `status` (or `undefined` when there was no HTTP response —
359
+ * an exhausted-network / per-attempt-timeout failure), and the normalized parsed error `body`.
360
+ * On the network/timeout path the original thrown error is preserved as `cause`. No error→HTTP
361
+ * translation or redaction — consumer-owned per ADR-0001.
362
+ *
363
+ * NOTE: the `readonly` modifiers below are a type-system-only guarantee; the runtime class
364
+ * (`errors.js`) sets these fields with plain assignment in the constructor.
365
+ */
366
+ export declare class ProjectEngineApiError extends Error {
367
+ /** The HTTP response status, or `undefined` when there was no HTTP response. */
368
+ readonly status: number | undefined;
369
+ /** The HTTP method of the failing request. */
370
+ readonly method: string;
371
+ /** The normalized parsed error body (`null` when empty/absent). */
372
+ readonly body: unknown;
373
+ constructor(
374
+ status: number | undefined,
375
+ method: string,
376
+ body: unknown,
377
+ options?: { cause?: unknown },
378
+ );
379
+ }
380
+
356
381
  // Re-export the generated contract types for consumers that want them directly.
357
382
  export type { paths, components };
package/src/index.js CHANGED
@@ -14,3 +14,4 @@
14
14
 
15
15
  export { createSerenityProjectEngineApiClient } from './client.js';
16
16
  export { createSerenityProjectEngineTransport } from './rest-transport.js';
17
+ export { ProjectEngineApiError } from './errors.js';
@@ -13,6 +13,7 @@
13
13
  // @ts-check
14
14
 
15
15
  import { createSerenityProjectEngineApiClient } from './client.js';
16
+ import { ProjectEngineApiError } from './errors.js';
16
17
 
17
18
  /**
18
19
  * @typedef {import('./client.js').SerenityProjectEngineApiClientOptions}
@@ -40,14 +41,17 @@ export function createSerenityProjectEngineTransport(options) {
40
41
 
41
42
  /**
42
43
  * The SINGLE seam where a failed call becomes a throw. It awaits the openapi-fetch result
43
- * promise here (so a network/timeout rejection also flows through this one point), and on a
44
- * non-2xx response throws. The typed client never throws on an HTTP error — it resolves to
45
- * `{ data, error, response }` with the parsed error body in `error` — so a non-2xx is turned
46
- * into a throw here; a 2xx returns the parsed body (or null for an empty body).
44
+ * promise here (so a network/timeout rejection also flows through this one point), and turns
45
+ * both failure paths into a {@link ProjectEngineApiError}:
47
46
  *
48
- * `status`, `method`, and the normalized `body` are computed locally at this site FIRST so the
49
- * follow-up ticket LLMO-5978 can swap ONLY the `new Error(...)` line below for
50
- * `new ProjectEngineApiError(status, method, body)` without reshaping anything else here.
47
+ * - a non-2xx HTTP response: the typed client never throws on an HTTP error it resolves to
48
+ * `{ data, error, response }` with the parsed error body in `error` so the non-2xx is turned
49
+ * into a throw here carrying `response.status` + the normalized `body`;
50
+ * - an exhausted-network / per-attempt-timeout failure: `createRetryingFetch` rethrew the last
51
+ * raw error after the retry budget, so the awaited promise rejects. There is no HTTP response
52
+ * ⇒ `status` is `undefined`; the original error is preserved as `cause`.
53
+ *
54
+ * A 2xx returns the parsed body (or null for an empty body).
51
55
  *
52
56
  * @template T
53
57
  * @param {string} method the HTTP method, for the error message
@@ -55,21 +59,26 @@ export function createSerenityProjectEngineTransport(options) {
55
59
  * openapi-fetch result
56
60
  * @returns {Promise<NonNullable<T> | null>} the parsed success body, or null for an empty body
57
61
  * (an empty-body operation resolves with null, never undefined)
62
+ * @throws {import('./errors.js').ProjectEngineApiError} on a non-2xx response or an
63
+ * exhausted-network / per-attempt-timeout failure
58
64
  */
59
65
  async function unwrap(method, resultPromise) {
60
- const { data, error, response } = await resultPromise;
66
+ let result;
67
+ try {
68
+ result = await resultPromise;
69
+ } catch (cause) {
70
+ // exhausted-network / per-attempt-timeout path: createRetryingFetch rethrew the last raw
71
+ // error after the retry budget. No HTTP response ⇒ status undefined; preserve the original
72
+ // as `cause`.
73
+ throw new ProjectEngineApiError(undefined, method, null, { cause });
74
+ }
75
+ const { data, error, response } = result;
61
76
  if (!response.ok) {
62
- const { status } = response;
63
- // openapi-fetch surfaces an empty error body as '' (not undefined); normalise it to null.
77
+ // openapi-fetch puts the parsed error body in `error`; fall back to `data`, then null.
64
78
  const rawBody = error ?? data ?? null;
65
- // `body` is computed here but not consumed by the plain Error below. LLMO-5978 swaps ONLY
66
- // the `new Error(...)` line for `new ProjectEngineApiError(status, method, body)` — status,
67
- // method, and this normalized body are all already in scope, so nothing else here changes.
68
- // eslint-disable-next-line no-unused-vars
79
+ // openapi-fetch surfaces an empty error body as '' (not undefined); normalise it to null.
69
80
  const body = rawBody === '' ? null : rawBody;
70
- // Message deliberately omits the request URL: it embeds path-param ids (workspace/
71
- // project/etc.) that error-reporter and log consumers should not receive by default.
72
- throw new Error(`Project Engine ${method} failed: ${status}`);
81
+ throw new ProjectEngineApiError(response.status, method, body);
73
82
  }
74
83
  return data ?? null;
75
84
  }