@bridge_gpt/mcp-server 0.2.19 → 0.2.20
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/README.md +6 -3
- package/build/agents.generated.js +1 -1
- package/build/commands.generated.js +4 -3
- package/build/conductor/local-merge.js +458 -95
- package/build/estimate-epic.js +84 -0
- package/build/executor/job-runner.js +151 -17
- package/build/executor/merge-job.js +84 -10
- package/build/executor/worker-finalization.js +98 -18
- package/build/index.js +1837 -397
- package/build/pipelines.generated.js +16 -20
- package/build/readme.generated.js +1 -1
- package/build/sfcc/client.js +192 -50
- package/build/sfcc/ocapi-write-faults.js +94 -0
- package/build/sfcc/permissions.js +7 -22
- package/build/sfcc/register.js +9 -0
- package/build/sfcc/write-grants.js +80 -0
- package/build/sfcc/write-guard.js +39 -0
- package/build/sfcc/write-result.js +47 -0
- package/build/sfcc/write-tool-common.js +85 -0
- package/build/sfcc/writes-custom-object-def.js +141 -0
- package/build/sfcc/writes-object-attribute-payloads.js +97 -0
- package/build/sfcc/writes-site-preference-payloads.js +59 -0
- package/build/sfcc/writes-site-preference.js +96 -0
- package/build/sfcc/writes-system-object-payloads.js +213 -0
- package/build/sfcc/writes-system-object.js +348 -0
- package/build/sfcc/writes.js +66 -0
- package/build/version.generated.js +1 -1
- package/package.json +3 -3
- package/pipelines/idea-to-ticket.json +7 -0
- package/pipelines/review-ticket.json +5 -18
- package/public/css/main.min.css +1583 -117
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +2792 -449
- package/public/js/main.min.js.map +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OCAPI write fault-mapping primitives (BAPI-582 foundation).
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the status/fault distinctions required by SFCC write plumbing so
|
|
5
|
+
* transport (client.ts) and future write tools do not each collapse write
|
|
6
|
+
* failures into a generic error. The known fault set below is the subset the
|
|
7
|
+
* ticket requires write handlers to be able to distinguish; unknown
|
|
8
|
+
* status/fault combinations are preserved verbatim rather than being coerced
|
|
9
|
+
* into one of the known types.
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Known (status, faultType) map
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** The exact (status → fault type) pairs the ticket requires callers to distinguish. */
|
|
15
|
+
const KNOWN_WRITE_FAULTS = {
|
|
16
|
+
400: "MalformedKeyParameterException",
|
|
17
|
+
404: "AttributeDefinitionNotFoundException",
|
|
18
|
+
409: "IfMatchRequiredException",
|
|
19
|
+
412: "InvalidIfMatchException",
|
|
20
|
+
};
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Fault extraction
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Extract the OCAPI fault type from a response body.
|
|
26
|
+
*
|
|
27
|
+
* Handles the common SFCC shapes:
|
|
28
|
+
* - `{ fault: { type: "..." } }`
|
|
29
|
+
* - `{ type: "..." }`
|
|
30
|
+
* - `{ fault: "..." }` (string-valued fault)
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` when no usable fault type string is present.
|
|
33
|
+
*/
|
|
34
|
+
export function extractOcapiFaultType(body) {
|
|
35
|
+
if (body === null || typeof body !== "object")
|
|
36
|
+
return undefined;
|
|
37
|
+
const record = body;
|
|
38
|
+
// `{ fault: { type } }` or `{ fault: "..." }`
|
|
39
|
+
const fault = record.fault;
|
|
40
|
+
if (typeof fault === "string" && fault.length > 0)
|
|
41
|
+
return fault;
|
|
42
|
+
if (fault !== null && typeof fault === "object") {
|
|
43
|
+
const faultType = fault.type;
|
|
44
|
+
if (typeof faultType === "string" && faultType.length > 0)
|
|
45
|
+
return faultType;
|
|
46
|
+
}
|
|
47
|
+
// `{ type: "..." }`
|
|
48
|
+
const type = record.type;
|
|
49
|
+
if (typeof type === "string" && type.length > 0)
|
|
50
|
+
return type;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Fault mapping
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* Map an OCAPI write response `(status, body)` into a structured fault.
|
|
58
|
+
*
|
|
59
|
+
* A fault is `known` only when BOTH the status and the extracted fault type
|
|
60
|
+
* match one of the ticket-required pairs. For everything else the numeric
|
|
61
|
+
* status and any extracted fault type are preserved without pretending the
|
|
62
|
+
* failure is one of the known faults.
|
|
63
|
+
*/
|
|
64
|
+
export function mapOcapiWriteFault(status, body) {
|
|
65
|
+
const faultType = extractOcapiFaultType(body);
|
|
66
|
+
const expected = KNOWN_WRITE_FAULTS[status];
|
|
67
|
+
const known = expected !== undefined && faultType === expected;
|
|
68
|
+
return {
|
|
69
|
+
status,
|
|
70
|
+
faultType,
|
|
71
|
+
known,
|
|
72
|
+
errorCode: known ? expected : "OCAPI_WRITE_FAULT",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Synthetic client-side bodies
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
/**
|
|
79
|
+
* Build an OCAPI-shaped synthetic body for the client-side missing-ETag case.
|
|
80
|
+
*
|
|
81
|
+
* Used by `ocapiPatch` when the GET round trip returned no ETag: rather than
|
|
82
|
+
* issuing a PATCH that SFCC would reject, the client synthesizes a
|
|
83
|
+
* `409 IfMatchRequiredException` body so callers see the same shape they would
|
|
84
|
+
* for a server-originated If-Match failure.
|
|
85
|
+
*/
|
|
86
|
+
export function buildSyntheticIfMatchRequiredBody(path) {
|
|
87
|
+
return {
|
|
88
|
+
fault: {
|
|
89
|
+
type: "IfMatchRequiredException",
|
|
90
|
+
message: `PATCH ${path} requires an ETag (If-Match) captured from the GET round trip, ` +
|
|
91
|
+
`but the GET response returned no ETag header. Cannot safely issue a conditional PATCH.`,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { ocapiGet } from "./client.js";
|
|
10
10
|
import { normalizeOcapiPage } from "./ocapi-shape.js";
|
|
11
11
|
import { DEFAULT_OCAPI_VERSION } from "./config.js";
|
|
12
|
+
import { formatOcapiWriteGrantJson } from "./write-grants.js";
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// OCAPI Settings JSON template
|
|
14
15
|
// Operators paste this in: Administration > Site Development > Open Commerce API Settings → Data API tab.
|
|
@@ -47,28 +48,12 @@ const OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify({
|
|
|
47
48
|
},
|
|
48
49
|
],
|
|
49
50
|
}, null, 2);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
resource_id: "/system_object_definitions",
|
|
58
|
-
methods: ["get", "put", "patch", "delete"],
|
|
59
|
-
read_attributes: "(**)",
|
|
60
|
-
write_attributes: "(**)",
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
resource_id: "/system_object_definitions/**",
|
|
64
|
-
methods: ["get", "put", "patch", "delete"],
|
|
65
|
-
read_attributes: "(**)",
|
|
66
|
-
write_attributes: "(**)",
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
}, null, 2);
|
|
51
|
+
// Write/import grants are sourced from the shared write-grants module so the
|
|
52
|
+
// JSON printed here is byte-identical to what future write handlers echo on a
|
|
53
|
+
// 403. This now covers all required write resource families:
|
|
54
|
+
// /system_object_definitions, /system_object_definitions/**,
|
|
55
|
+
// /custom_object_definitions/**, and /site_preferences/**.
|
|
56
|
+
const OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => formatOcapiWriteGrantJson(ocapiVersion);
|
|
72
57
|
// ---------------------------------------------------------------------------
|
|
73
58
|
// Tool implementation
|
|
74
59
|
// ---------------------------------------------------------------------------
|
package/build/sfcc/register.js
CHANGED
|
@@ -13,6 +13,7 @@ import { checkPermissionsTool } from "./permissions.js";
|
|
|
13
13
|
import { registerSystemObjectReadTools } from "./reads-system-object.js";
|
|
14
14
|
import { registerSfccCustomObjectDefReadTools } from "./reads-custom-object-def.js";
|
|
15
15
|
import { registerSitePreferenceTools } from "./reads-site-preference.js";
|
|
16
|
+
import { registerSfccWriteTools } from "./writes.js";
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Registration
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -69,5 +70,13 @@ export function registerSfccTools(registerTool, deps) {
|
|
|
69
70
|
gateDeps,
|
|
70
71
|
getDocsDir: deps.getDocsDir,
|
|
71
72
|
});
|
|
73
|
+
// SFCC write registration seam (BAPI-582). No write tools ship yet — future
|
|
74
|
+
// write-surface tickets register here. Gated behind the same includeReadTools
|
|
75
|
+
// branch so writes become default-on with reads whenever the sfcc profile
|
|
76
|
+
// group is active.
|
|
77
|
+
registerSfccWriteTools(registerTool, {
|
|
78
|
+
gateDeps,
|
|
79
|
+
getDocsDir: deps.getDocsDir,
|
|
80
|
+
});
|
|
72
81
|
}
|
|
73
82
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared OCAPI write-grant JSON helpers (BAPI-582 foundation).
|
|
3
|
+
*
|
|
4
|
+
* This module is the single source of truth for the OCAPI write-grant JSON that
|
|
5
|
+
* is both echoed by future write handlers on a 403 and printed by the
|
|
6
|
+
* `check_permissions` diagnostic. Keeping one definition guarantees the
|
|
7
|
+
* remediation JSON an operator sees is identical no matter which surface it
|
|
8
|
+
* comes from.
|
|
9
|
+
*/
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Write resource families
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* The OCAPI write resource families required by the SFCC write surface.
|
|
15
|
+
* Defined once here so the grant JSON and any future validation share the list.
|
|
16
|
+
*/
|
|
17
|
+
export const OCAPI_WRITE_RESOURCE_IDS = [
|
|
18
|
+
"/system_object_definitions",
|
|
19
|
+
"/system_object_definitions/**",
|
|
20
|
+
"/custom_object_definitions/**",
|
|
21
|
+
"/site_preferences/**",
|
|
22
|
+
];
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Grant JSON builders
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Build the OCAPI Settings write-grant object for the given version.
|
|
28
|
+
*
|
|
29
|
+
* The `clientIdPlaceholder` stays as `<YOUR_CLIENT_ID>` by default so the output
|
|
30
|
+
* is safe to print and paste — the real resolved client_id is never substituted.
|
|
31
|
+
*/
|
|
32
|
+
export function buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
|
|
33
|
+
return {
|
|
34
|
+
_v: ocapiVersion,
|
|
35
|
+
clients: [
|
|
36
|
+
{
|
|
37
|
+
client_id: clientIdPlaceholder,
|
|
38
|
+
resources: OCAPI_WRITE_RESOURCE_IDS.map((resource_id) => ({
|
|
39
|
+
resource_id,
|
|
40
|
+
methods: ["get", "put", "patch", "delete"],
|
|
41
|
+
read_attributes: "(**)",
|
|
42
|
+
write_attributes: "(**)",
|
|
43
|
+
})),
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Pretty-printed JSON string of the write-grant settings. */
|
|
49
|
+
export function formatOcapiWriteGrantJson(ocapiVersion, clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
|
|
50
|
+
return JSON.stringify(buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder), null, 2);
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// 403 write-grant UX
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/**
|
|
56
|
+
* Build the human-facing 403 remediation text for a failed write operation.
|
|
57
|
+
* Includes the 403 status, the failed operation/path, and the exact write-grant
|
|
58
|
+
* JSON operators must paste in Business Manager.
|
|
59
|
+
*/
|
|
60
|
+
export function buildOcapiWriteGrant403Text(params) {
|
|
61
|
+
const { operation, path, ocapiVersion, body } = params;
|
|
62
|
+
const bodyLine = body === undefined
|
|
63
|
+
? ""
|
|
64
|
+
: `\nResponse body:\n${JSON.stringify(body, null, 2)}\n`;
|
|
65
|
+
return (`HTTP 403: OCAPI write access denied for ${operation} ${path}.\n` +
|
|
66
|
+
bodyLine +
|
|
67
|
+
`\nTo grant write access, paste the JSON below in Business Manager:\n` +
|
|
68
|
+
` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
|
|
69
|
+
`${formatOcapiWriteGrantJson(ocapiVersion)}\n\n` +
|
|
70
|
+
`Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* MCP-shaped wrapper around `buildOcapiWriteGrant403Text` so future write
|
|
74
|
+
* handlers can return the grant UX without re-implementing content wrapping.
|
|
75
|
+
*/
|
|
76
|
+
export function writeGrantForbiddenResult(params) {
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: buildOcapiWriteGrant403Text(params) }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable sandbox-only write guard (BAPI-582 foundation).
|
|
3
|
+
*
|
|
4
|
+
* Future SFCC write handlers MUST call `rejectIfNotSandboxForWrite`
|
|
5
|
+
* immediately after input parsing and BEFORE any payload construction or OCAPI
|
|
6
|
+
* mutation. A non-null return value is
|
|
7
|
+
* the pre-formatted MCP rejection to return directly; a null return means the
|
|
8
|
+
* effective instance is a sandbox and the handler may proceed.
|
|
9
|
+
*/
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Wrap text in the standard MCP text-result shape. */
|
|
14
|
+
function textResult(text) {
|
|
15
|
+
return { content: [{ type: "text", text }] };
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Guard
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Reject any write targeting a non-sandbox instance.
|
|
22
|
+
*
|
|
23
|
+
* SFCC write tools are sandbox-only in this foundation. The effective instance
|
|
24
|
+
* defaults to `"sandbox"` when `undefined` (some schemas default the value
|
|
25
|
+
* elsewhere), so an omitted instance is allowed. Any other value returns a
|
|
26
|
+
* structured JSON validation error naming the rejected instance; a sandbox
|
|
27
|
+
* returns `null` so the caller proceeds.
|
|
28
|
+
*/
|
|
29
|
+
export function rejectIfNotSandboxForWrite(instance) {
|
|
30
|
+
const effective = instance === undefined ? "sandbox" : instance;
|
|
31
|
+
if (effective === "sandbox")
|
|
32
|
+
return null;
|
|
33
|
+
return textResult(JSON.stringify({
|
|
34
|
+
error: "VALIDATION_ERROR",
|
|
35
|
+
status: 400,
|
|
36
|
+
message: `SFCC write tools are sandbox-only. Refusing to write against instance ` +
|
|
37
|
+
`'${effective}'. Re-run the write against a developer sandbox instance.`,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared write-result formatter (BAPI-582 foundation).
|
|
3
|
+
*
|
|
4
|
+
* This is the single formatting path for all future SFCC write tools. Handlers
|
|
5
|
+
* pass the raw `OcapiGetResult` from `ocapiPut`/`ocapiPatch` here rather than
|
|
6
|
+
* inventing their own 403/fault/success response logic, guaranteeing a
|
|
7
|
+
* consistent surface across every write tool.
|
|
8
|
+
*/
|
|
9
|
+
import { writeGrantForbiddenResult } from "./write-grants.js";
|
|
10
|
+
import { DEFAULT_OCAPI_VERSION } from "./config.js";
|
|
11
|
+
/** Wrap text in the standard MCP text-result shape. */
|
|
12
|
+
function textResult(text) {
|
|
13
|
+
return { content: [{ type: "text", text }] };
|
|
14
|
+
}
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Formatter
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/**
|
|
19
|
+
* Format an OCAPI write result into the standard MCP tool response.
|
|
20
|
+
*
|
|
21
|
+
* - `403` → the paste-ready write-grant remediation (via write-grants.ts).
|
|
22
|
+
* - success → JSON with `status`, `outcome`, and `body`.
|
|
23
|
+
* - other failure → JSON with `error: "OCAPI_WRITE_ERROR"`, `status`, `fault`, `body`.
|
|
24
|
+
*/
|
|
25
|
+
export function formatOcapiWriteToolResult(result, operation, path, ocapiVersion = DEFAULT_OCAPI_VERSION) {
|
|
26
|
+
if (result.status === 403) {
|
|
27
|
+
return writeGrantForbiddenResult({
|
|
28
|
+
operation,
|
|
29
|
+
path,
|
|
30
|
+
ocapiVersion,
|
|
31
|
+
body: result.body,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (result.ok) {
|
|
35
|
+
return textResult(JSON.stringify({
|
|
36
|
+
status: result.status,
|
|
37
|
+
outcome: result.outcome,
|
|
38
|
+
body: result.body,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
return textResult(JSON.stringify({
|
|
42
|
+
error: "OCAPI_WRITE_ERROR",
|
|
43
|
+
status: result.status,
|
|
44
|
+
fault: result.fault,
|
|
45
|
+
body: result.body,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the SFCC write MCP tools (BAPI-584).
|
|
3
|
+
*
|
|
4
|
+
* The two write-surface modules (`writes-custom-object-def.ts` and
|
|
5
|
+
* `writes-site-preference.ts`) both need the same small set of primitives:
|
|
6
|
+
* the destructive tool annotations, the MCP text-result shape, a consistent
|
|
7
|
+
* set of safe pre-transport error envelopes (validation / unexpected), and a
|
|
8
|
+
* single path-segment encoder. Centralizing them here guarantees every write
|
|
9
|
+
* tool returns an identical envelope shape and never leaks a stack trace or a
|
|
10
|
+
* raw thrown value to the caller.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Annotations
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Annotations shared by every SFCC write tool. Writes are mutating and NOT
|
|
18
|
+
* read-only or idempotent; `openWorldHint` mirrors the read tools (OCAPI is an
|
|
19
|
+
* external system).
|
|
20
|
+
*/
|
|
21
|
+
export const WRITE_ANNOTATIONS = {
|
|
22
|
+
readOnlyHint: false,
|
|
23
|
+
destructiveHint: true,
|
|
24
|
+
idempotentHint: false,
|
|
25
|
+
openWorldHint: true,
|
|
26
|
+
};
|
|
27
|
+
/** Wrap text in the standard MCP text-result shape. */
|
|
28
|
+
export function textResult(text) {
|
|
29
|
+
return { content: [{ type: "text", text }] };
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Pre-transport error envelopes
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Convert a Zod validation failure into a safe `VALIDATION_ERROR` envelope.
|
|
36
|
+
* The flattened issues are surfaced so callers can see which field failed,
|
|
37
|
+
* without exposing any thrown-error internals.
|
|
38
|
+
*/
|
|
39
|
+
export function zodValidationEnvelope(err) {
|
|
40
|
+
return textResult(JSON.stringify({
|
|
41
|
+
error: "VALIDATION_ERROR",
|
|
42
|
+
status: 400,
|
|
43
|
+
message: "Input failed schema validation before any OCAPI call.",
|
|
44
|
+
issues: err.issues.map((issue) => ({
|
|
45
|
+
path: issue.path.join("."),
|
|
46
|
+
message: issue.message,
|
|
47
|
+
})),
|
|
48
|
+
}, null, 2));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A plain (non-Zod) validation rejection, e.g. a body/URL field mismatch that
|
|
52
|
+
* the schema alone cannot express. Same envelope shape as the Zod variant.
|
|
53
|
+
*/
|
|
54
|
+
export function validationEnvelope(message) {
|
|
55
|
+
return textResult(JSON.stringify({ error: "VALIDATION_ERROR", status: 400, message }, null, 2));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Safe fallback envelope for an unexpected thrown error inside a write handler
|
|
59
|
+
* (typically a transport-layer throw). Never includes a stack trace or the raw
|
|
60
|
+
* thrown value.
|
|
61
|
+
*/
|
|
62
|
+
export function unexpectedEnvelope() {
|
|
63
|
+
return textResult(JSON.stringify({
|
|
64
|
+
error: "INTERNAL_ERROR",
|
|
65
|
+
status: 500,
|
|
66
|
+
message: "Unexpected SFCC write tool failure.",
|
|
67
|
+
}, null, 2));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Map a pre-transport error to its envelope: Zod validation errors become a
|
|
71
|
+
* `VALIDATION_ERROR`, everything else becomes the generic `INTERNAL_ERROR`.
|
|
72
|
+
* Used for the `.parse()` boundary before any OCAPI mutation.
|
|
73
|
+
*/
|
|
74
|
+
export function preTransportErrorEnvelope(err) {
|
|
75
|
+
if (err instanceof z.ZodError)
|
|
76
|
+
return zodValidationEnvelope(err);
|
|
77
|
+
return unexpectedEnvelope();
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Path encoding
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/** URL-encode a single OCAPI path segment. */
|
|
83
|
+
export function encodedSegment(segment) {
|
|
84
|
+
return encodeURIComponent(segment);
|
|
85
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SFCC custom-object attribute-definition write tools (BAPI-584).
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* custom_object_definition_attribute_create — PUT /custom_object_definitions/{type}/attribute_definitions/{id}
|
|
6
|
+
* custom_object_definition_attribute_update — PATCH /custom_object_definitions/{type}/attribute_definitions/{id}
|
|
7
|
+
*
|
|
8
|
+
* Both tools write ATTRIBUTE DEFINITIONS on an already-known custom object type.
|
|
9
|
+
* Custom object TYPE creation is intentionally NOT attempted: OCAPI cannot
|
|
10
|
+
* create custom object types (that is a v2 metadata-import capability), so the
|
|
11
|
+
* type must pre-exist. Mirrors reads-custom-object-def.ts.
|
|
12
|
+
*
|
|
13
|
+
* Every handler is call-time gated by `withSfccGate`, sandbox-guarded via
|
|
14
|
+
* `rejectIfNotSandboxForWrite`, and routes its OCAPI result through
|
|
15
|
+
* `formatOcapiWriteToolResult` (403 → paste-ready grant JSON; 409/412 conflicts
|
|
16
|
+
* surfaced verbatim).
|
|
17
|
+
*/
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { ocapiPut, ocapiPatch } from "./client.js";
|
|
20
|
+
import { withSfccGate } from "./tool-wrapper.js";
|
|
21
|
+
import { rejectIfNotSandboxForWrite } from "./write-guard.js";
|
|
22
|
+
import { formatOcapiWriteToolResult } from "./write-result.js";
|
|
23
|
+
import { objectAttributeDefinitionCreateBodySchema, objectAttributeDefinitionPatchBodySchema, buildObjectAttributeDefinitionCreatePayload, buildObjectAttributeDefinitionPatchPayload, } from "./writes-object-attribute-payloads.js";
|
|
24
|
+
import { WRITE_ANNOTATIONS, encodedSegment, preTransportErrorEnvelope, unexpectedEnvelope, validationEnvelope, } from "./write-tool-common.js";
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Input schemas
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const INSTANCE_DESCRIBE = "OCAPI instance context. SFCC writes are sandbox-only; omit for sandbox. Any " +
|
|
29
|
+
"other value is rejected before OCAPI is called.";
|
|
30
|
+
const createCustomObjectAttributeDefinitionInput = z.object({
|
|
31
|
+
object_type: z
|
|
32
|
+
.string()
|
|
33
|
+
.describe('Known custom object type identifier (must already exist). OCAPI cannot ' +
|
|
34
|
+
"enumerate or create custom object types — only attribute definitions on a known type."),
|
|
35
|
+
attribute_id: z
|
|
36
|
+
.string()
|
|
37
|
+
.describe("URL attribute-definition id. If the body also carries `id`, it must match."),
|
|
38
|
+
definition: objectAttributeDefinitionCreateBodySchema.describe("ObjectAttributeDefinition body; `value_type` is required for a create."),
|
|
39
|
+
instance: z.string().optional().describe(INSTANCE_DESCRIBE),
|
|
40
|
+
});
|
|
41
|
+
const updateCustomObjectAttributeDefinitionInput = z.object({
|
|
42
|
+
object_type: z
|
|
43
|
+
.string()
|
|
44
|
+
.describe("Known custom object type identifier (must already exist). OCAPI cannot create types."),
|
|
45
|
+
attribute_id: z.string().describe("URL attribute-definition id to update."),
|
|
46
|
+
patch: objectAttributeDefinitionPatchBodySchema.describe("Partial ObjectAttributeDefinition body; must change at least one field."),
|
|
47
|
+
instance: z.string().optional().describe(INSTANCE_DESCRIBE),
|
|
48
|
+
});
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Path builder
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/** Build the attribute-definition resource path with both dynamic segments encoded. */
|
|
53
|
+
export function customObjectAttributeDefinitionPath(objectType, attributeId) {
|
|
54
|
+
return (`/custom_object_definitions/${encodedSegment(objectType)}` +
|
|
55
|
+
`/attribute_definitions/${encodedSegment(attributeId)}`);
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Handlers
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
export function buildCreateCustomObjectAttributeDefinitionHandler(gateDeps) {
|
|
61
|
+
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = createCustomObjectAttributeDefinitionInput.parse(args);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return preTransportErrorEnvelope(err);
|
|
68
|
+
}
|
|
69
|
+
const guard = rejectIfNotSandboxForWrite(parsed.instance);
|
|
70
|
+
if (guard)
|
|
71
|
+
return guard;
|
|
72
|
+
// The URL attribute id is authoritative; a mismatched body `id` is a caller bug.
|
|
73
|
+
if (parsed.definition.id !== undefined && parsed.definition.id !== parsed.attribute_id) {
|
|
74
|
+
return validationEnvelope(`Body id '${parsed.definition.id}' does not match URL attribute_id '${parsed.attribute_id}'.`);
|
|
75
|
+
}
|
|
76
|
+
const path = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
|
|
77
|
+
const body = buildObjectAttributeDefinitionCreatePayload(parsed.attribute_id, parsed.definition);
|
|
78
|
+
try {
|
|
79
|
+
const result = await ocapiPut(path, body, credentials);
|
|
80
|
+
return formatOcapiWriteToolResult(result, "PUT", path);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return unexpectedEnvelope();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps) {
|
|
88
|
+
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = updateCustomObjectAttributeDefinitionInput.parse(args);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return preTransportErrorEnvelope(err);
|
|
95
|
+
}
|
|
96
|
+
const guard = rejectIfNotSandboxForWrite(parsed.instance);
|
|
97
|
+
if (guard)
|
|
98
|
+
return guard;
|
|
99
|
+
const path = customObjectAttributeDefinitionPath(parsed.object_type, parsed.attribute_id);
|
|
100
|
+
const body = buildObjectAttributeDefinitionPatchPayload(parsed.patch);
|
|
101
|
+
try {
|
|
102
|
+
// ocapiPatch performs the GET-then-If-Match ETag round trip; 409/412
|
|
103
|
+
// (IfMatchRequired / InvalidIfMatch) responses flow through formatOcapiWriteToolResult.
|
|
104
|
+
const result = await ocapiPatch(path, body, credentials);
|
|
105
|
+
return formatOcapiWriteToolResult(result, "PATCH", path);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return unexpectedEnvelope();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Registration
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
/** Tool names registered by this module. */
|
|
116
|
+
export const CUSTOM_OBJECT_DEF_WRITE_TOOL_NAMES = [
|
|
117
|
+
"custom_object_definition_attribute_create",
|
|
118
|
+
"custom_object_definition_attribute_update",
|
|
119
|
+
];
|
|
120
|
+
/**
|
|
121
|
+
* Register the two SFCC custom-object attribute-definition write tools.
|
|
122
|
+
* Called from `registerSfccWriteTools` — no direct `index.ts` edits needed.
|
|
123
|
+
*/
|
|
124
|
+
export function registerSfccCustomObjectDefWriteTools(registerTool, deps) {
|
|
125
|
+
const { gateDeps } = deps;
|
|
126
|
+
registerTool("custom_object_definition_attribute_create", {
|
|
127
|
+
description: "Create an attribute definition on a KNOWN custom object type via " +
|
|
128
|
+
"PUT /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, " +
|
|
129
|
+
"destructive; the type must pre-exist (OCAPI cannot create types). Echoes paste-ready " +
|
|
130
|
+
"grant JSON on 403.",
|
|
131
|
+
inputSchema: createCustomObjectAttributeDefinitionInput,
|
|
132
|
+
annotations: WRITE_ANNOTATIONS,
|
|
133
|
+
}, buildCreateCustomObjectAttributeDefinitionHandler(gateDeps));
|
|
134
|
+
registerTool("custom_object_definition_attribute_update", {
|
|
135
|
+
description: "Update an attribute definition on a KNOWN custom object type via an ETag-conditional " +
|
|
136
|
+
"PATCH /custom_object_definitions/{type}/attribute_definitions/{id}. Sandbox-only, " +
|
|
137
|
+
"destructive; surfaces 409/412 conflicts and echoes grant JSON on 403.",
|
|
138
|
+
inputSchema: updateCustomObjectAttributeDefinitionInput,
|
|
139
|
+
annotations: WRITE_ANNOTATIONS,
|
|
140
|
+
}, buildUpdateCustomObjectAttributeDefinitionHandler(gateDeps));
|
|
141
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure payload builders + schemas for OCAPI ObjectAttributeDefinition writes
|
|
3
|
+
* (BAPI-584).
|
|
4
|
+
*
|
|
5
|
+
* This module performs NO network calls. It exists to keep the
|
|
6
|
+
* ObjectAttributeDefinition request-body construction decoupled from OCAPI
|
|
7
|
+
* transport (client.ts) so the write handlers stay thin and the body shape is
|
|
8
|
+
* independently testable.
|
|
9
|
+
*
|
|
10
|
+
* The ObjectAttributeDefinition document class is shared by OCAPI system-object
|
|
11
|
+
* and custom-object attribute-definition writes. BAPI-583 (system-object
|
|
12
|
+
* attribute writes) is the sibling surface; if/when it lands it can reuse these
|
|
13
|
+
* builders rather than re-deriving them. The schema is intentionally permissive
|
|
14
|
+
* (`.passthrough()`): OCAPI defines many optional attribute properties and this
|
|
15
|
+
* layer must not reject a valid one it does not happen to enumerate.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Value types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* The OCAPI ObjectAttributeDefinition `value_type` enum. Kept broad to cover
|
|
23
|
+
* the documented set; unknown-but-valid future types are not the common case,
|
|
24
|
+
* and the write still fails loud at OCAPI if a type is rejected server-side.
|
|
25
|
+
*/
|
|
26
|
+
export const objectAttributeValueTypeSchema = z.enum([
|
|
27
|
+
"string",
|
|
28
|
+
"int",
|
|
29
|
+
"double",
|
|
30
|
+
"boolean",
|
|
31
|
+
"date",
|
|
32
|
+
"datetime",
|
|
33
|
+
"email",
|
|
34
|
+
"enum_of_int",
|
|
35
|
+
"enum_of_string",
|
|
36
|
+
"html",
|
|
37
|
+
"image",
|
|
38
|
+
"money",
|
|
39
|
+
"password",
|
|
40
|
+
"quantity",
|
|
41
|
+
"set_of_int",
|
|
42
|
+
"set_of_string",
|
|
43
|
+
"set_of_double",
|
|
44
|
+
"text",
|
|
45
|
+
]);
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Create / patch body schemas
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Body for creating an attribute definition (PUT). OCAPI requires `value_type`
|
|
51
|
+
* for a create; `id` is optional here because the URL segment is authoritative
|
|
52
|
+
* (the handler reconciles the two). All other ObjectAttributeDefinition
|
|
53
|
+
* properties pass through unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export const objectAttributeDefinitionCreateBodySchema = z
|
|
56
|
+
.object({
|
|
57
|
+
id: z.string().min(1).optional(),
|
|
58
|
+
value_type: objectAttributeValueTypeSchema,
|
|
59
|
+
})
|
|
60
|
+
.passthrough();
|
|
61
|
+
/**
|
|
62
|
+
* Body for updating an attribute definition (PATCH). Every property is
|
|
63
|
+
* optional, but the patch must change at least one field — an empty patch is
|
|
64
|
+
* rejected before the ETag round trip.
|
|
65
|
+
*/
|
|
66
|
+
export const objectAttributeDefinitionPatchBodySchema = z
|
|
67
|
+
.object({
|
|
68
|
+
value_type: objectAttributeValueTypeSchema.optional(),
|
|
69
|
+
})
|
|
70
|
+
.passthrough()
|
|
71
|
+
.superRefine((body, ctx) => {
|
|
72
|
+
if (Object.keys(body).length === 0) {
|
|
73
|
+
ctx.addIssue({
|
|
74
|
+
code: z.ZodIssueCode.custom,
|
|
75
|
+
message: "Patch body must contain at least one attribute-definition field to update.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Builders (pure)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Build the create (PUT) payload. Returns a shallow copy of the caller body
|
|
84
|
+
* with `id` forced to the authoritative URL attribute id, so the URL and body
|
|
85
|
+
* can never disagree. No wrapper object is added — OCAPI expects the
|
|
86
|
+
* ObjectAttributeDefinition document at the top level.
|
|
87
|
+
*/
|
|
88
|
+
export function buildObjectAttributeDefinitionCreatePayload(attributeId, body) {
|
|
89
|
+
return { ...body, id: attributeId };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build the patch (PATCH) payload — a shallow copy of the caller body with no
|
|
93
|
+
* wrapper. The ETag round trip is performed by the transport layer, not here.
|
|
94
|
+
*/
|
|
95
|
+
export function buildObjectAttributeDefinitionPatchPayload(body) {
|
|
96
|
+
return { ...body };
|
|
97
|
+
}
|