@bridge_gpt/mcp-server 0.2.19 → 0.2.21

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.
Files changed (35) hide show
  1. package/README.md +6 -3
  2. package/build/agents.generated.js +1 -1
  3. package/build/commands.generated.js +4 -3
  4. package/build/conductor/local-merge.js +458 -95
  5. package/build/estimate-epic.js +84 -0
  6. package/build/executor/job-runner.js +151 -17
  7. package/build/executor/merge-job.js +84 -10
  8. package/build/executor/worker-finalization.js +98 -18
  9. package/build/index.js +1843 -401
  10. package/build/pipelines.generated.js +16 -20
  11. package/build/readme.generated.js +1 -1
  12. package/build/review-tickets.js +15 -5
  13. package/build/sfcc/client.js +192 -50
  14. package/build/sfcc/ocapi-write-faults.js +94 -0
  15. package/build/sfcc/permissions.js +7 -22
  16. package/build/sfcc/register.js +9 -0
  17. package/build/sfcc/write-grants.js +80 -0
  18. package/build/sfcc/write-guard.js +39 -0
  19. package/build/sfcc/write-result.js +47 -0
  20. package/build/sfcc/write-tool-common.js +85 -0
  21. package/build/sfcc/writes-custom-object-def.js +141 -0
  22. package/build/sfcc/writes-object-attribute-payloads.js +97 -0
  23. package/build/sfcc/writes-site-preference-payloads.js +59 -0
  24. package/build/sfcc/writes-site-preference.js +96 -0
  25. package/build/sfcc/writes-system-object-payloads.js +213 -0
  26. package/build/sfcc/writes-system-object.js +348 -0
  27. package/build/sfcc/writes.js +66 -0
  28. package/build/version.generated.js +1 -1
  29. package/package.json +3 -3
  30. package/pipelines/idea-to-ticket.json +7 -0
  31. package/pipelines/review-ticket.json +5 -18
  32. package/public/css/main.min.css +1583 -117
  33. package/public/css/main.min.css.map +1 -1
  34. package/public/js/main.min.js +2792 -449
  35. 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
- const OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => JSON.stringify({
51
- _v: ocapiVersion,
52
- clients: [
53
- {
54
- client_id: "<YOUR_CLIENT_ID>",
55
- resources: [
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
  // ---------------------------------------------------------------------------
@@ -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
+ }