diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..bf4d391 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# oxfmt (via `make codeclean`) reads this file. +# +# oxfmt formats fenced code blocks inside Markdown. It parses ```handlebars +# blocks as JavaScript and rewrites them, which changes documented template +# syntax rather than just its layout — `{{ 0.firstName }}` becomes +# `{{[0].firstName}}`, a different Handlebars expression. The mass-operations +# templating reference documents exactly what the runner accepts, so it must +# not be reformatted until oxfmt leaves handlebars blocks alone. +use-crystallize/skills/mass-operations/references/templating.md diff --git a/use-crystallize/skills/mass-operations/SKILL.md b/use-crystallize/skills/mass-operations/SKILL.md index 4f40f71..e4a0612 100644 --- a/use-crystallize/skills/mass-operations/SKILL.md +++ b/use-crystallize/skills/mass-operations/SKILL.md @@ -25,9 +25,9 @@ outside API rate limits, coordinated multi-step changes, and large-scale content ## Decide the execution path first -| Situation | Path | -| --- | --- | -| User has the Crystallize CLI, or is doing a production migration | **CLI** (default — recommended) | +| Situation | Path | +| ------------------------------------------------------------------ | ------------------------------------------- | +| User has the Crystallize CLI, or is doing a production migration | **CLI** (default — recommended) | | Browser/app context, CI without CLI, or building tooling around it | **Raw API** (see `references/lifecycle.md`) | ## Workflow @@ -46,22 +46,22 @@ Component IDs that don't exist on the target shape are the single most common ca ```json { - "version": "1.0.0", - "operations": [ - { - "intent": "piece/upsert", - "identifier": "rating-system", - "name": "Rating System", - "components": [ + "version": "1.0.0", + "operations": [ { - "id": "name", - "name": "Name", - "type": "singleLine", - "config": { "singleLine": { "required": true } } + "intent": "piece/upsert", + "identifier": "rating-system", + "name": "Rating System", + "components": [ + { + "id": "name", + "name": "Name", + "type": "singleLine", + "config": { "singleLine": { "required": true } } + } + ] } - ] - } - ] + ] } ``` @@ -114,18 +114,18 @@ dependency is positional — the referencing operation has to come later in the that creates its target. This is a universal rule: it applies to content exactly as it applies to the content model. There is no deferred resolution and no second pass. -| Reference | Must already exist | -| --- | --- | -| `shape/upsert` choice → piece | the `piece/upsert` | -| item's `shapeIdentifier` | the `shape/upsert` | -| `tree.parentId` | the parent `folder/create` (or `{{ defaults.rootItemId }}`) | -| `topicIds` on an item | the `topic/*` operations | -| `itemRelations` content | every item it points at | -| `item/updateComponent/*` | the item, and the component on its shape | -| `product/variant/*`, `…/price/modify`, `…/stock/modify` | the product, price variant, stock location | -| `item/flow/stage/addItems` | the `flow/*` defining that stage | -| `order`/`subscription-contract` customer link | the `customer/upsert` | -| any `{{ myRef.… }}` | the operation carrying that `_ref` | +| Reference | Must already exist | +| ------------------------------------------------------- | ----------------------------------------------------------- | +| `shape/upsert` choice → piece | the `piece/upsert` | +| item's `shapeIdentifier` | the `shape/upsert` | +| `tree.parentId` | the parent `folder/create` (or `{{ defaults.rootItemId }}`) | +| `topicIds` on an item | the `topic/*` operations | +| `itemRelations` content | every item it points at | +| `item/updateComponent/*` | the item, and the component on its shape | +| `product/variant/*`, `…/price/modify`, `…/stock/modify` | the product, price variant, stock location | +| `item/flow/stage/addItems` | the `flow/*` defining that stage | +| `order`/`subscription-contract` customer link | the `customer/upsert` | +| any `{{ myRef.… }}` | the operation carrying that `_ref` | Chain with `_ref` where there is no natural identifier, and with a shared `identifier` / `resourceIdentifier` where there is. @@ -174,7 +174,7 @@ logs, not to you. Validate locally to see them. ## Rules that prevent most failures - **An item `upsert` with neither `itemId` nor `resourceIdentifier` always CREATES** — on every run. - `externalReference` is *not* a lookup key. This is the number-one duplicate-data trap: give every + `externalReference` is _not_ a lookup key. This is the number-one duplicate-data trap: give every `product|document|folder /upsert` a `resourceIdentifier`. Identifier-keyed upserts (`piece`, `shape`, `customer`, `customer/group`, `pricelist`, `flow`, `product/variant`) are genuinely idempotent; `order/upsert` and `subscription-contract/upsert` are not unless you pass a real `id`. @@ -182,8 +182,8 @@ logs, not to you. Validate locally to see them. uploads fine, then fails the whole task at execution and abandons every remaining operation. It is the only one of the 59 intents in this position. Unpublish via the Core API `unpublishItem` mutation instead. -- **Prefer `upsert` for re-runnability — but upserts derive from the *create* input schema**, so they - need the *full* create payload, not a sparse patch. `product/upsert` requires `tree`, `vatTypeId`, and +- **Prefer `upsert` for re-runnability — but upserts derive from the _create_ input schema**, so they + need the _full_ create payload, not a sparse patch. `product/upsert` requires `tree`, `vatTypeId`, and `variants` exactly as `product/create` does. (`product/variant/update` is the exception — it's `.partial()`, so it genuinely accepts a sparse patch.) - **At `version: "1.0.0"` there is no upsert return-shape trap.** The v1.0.0 converters normalise every @@ -203,9 +203,9 @@ logs, not to you. Validate locally to see them. `{{ ... }}` expression, nothing else. This is why `{{ defaults.vatTypeIds.[0] }}` and `{{ defaults.rootItemId }}` work where a literal placeholder like `"TODO"` fails validation. - **A handlebars expression that fails to render is written through literally.** The renderer catches - the error, logs it, and returns the *raw template string* — so a bad reference silently stores + the error, logs it, and returns the _raw template string_ — so a bad reference silently stores `{{ myRef.id }}` as data rather than failing the operation. -- **`item/paths/set*` needs two operations to converge.** If anything must be removed, that run *only* +- **`item/paths/set*` needs two operations to converge.** If anything must be removed, that run _only_ removes; the add is expected as a separate operation. Not atomic, despite the name. - **Respect the domain limits — the schema does not.** A file that exceeds one validates, uploads, and fails at execution, or worse succeeds with altered data. **One invalid operation rejects the entire diff --git a/use-crystallize/skills/mass-operations/references/intents.md b/use-crystallize/skills/mass-operations/references/intents.md index 8ad4445..8c47bb1 100644 --- a/use-crystallize/skills/mass-operations/references/intents.md +++ b/use-crystallize/skills/mass-operations/references/intents.md @@ -36,57 +36,57 @@ Unpublish via the Core API `unpublishItem` mutation instead. ## Required fields per intent Derived by parsing empty payloads against the real schema. `_ref` is optional on every intent. -"+ id" means `checkResourceIdentifierOrId` applies at the *schema* level: supply `itemId`/`id`/`topicId` +"+ id" means `checkResourceIdentifierOrId` applies at the _schema_ level: supply `itemId`/`id`/`topicId` **or** `resourceIdentifier`. Whether the runner then honours `resourceIdentifier` is a separate question — see the next section. -| Intent | Required fields | -| --- | --- | -| `folder/create` | `name`, `shapeIdentifier`, `tree`, `language` | -| `folder/update` | as create, + id | -| `folder/upsert` | as create | -| `document/create` | `name`, `shapeIdentifier`, `tree`, `language` | -| `document/update` | as create, + id | -| `document/upsert` | as create | -| `product/create` | `name`, `shapeIdentifier`, `tree`, `vatTypeId`, `variants`, `language` | -| `product/update` | as create, + id | -| `product/upsert` | as create | -| `item/updateComponent/item` | `language`, `component`, + id — **runner requires `itemId` specifically** | -| `item/updateComponent/sku` | `language`, `component`, `sku` | -| `item/publish` | `language`, + id — **runner requires `itemId` specifically** | -| `item/unpublish` | schema-valid, **not implemented** — see above | -| `item/delete` | + id. Passing *both* `itemId` and `resourceIdentifier` throws | -| `item/flow/stage/addItems` | `items` (min 1), `stageIdentifier`. Optional `moveFromFlowIdentifier`, `actionConfig` | -| `shape/create` | `identifier`, `name` | -| `shape/update`, `shape/upsert` | `identifier`, `name` | -| `piece/create`, `piece/update`, `piece/upsert` | `identifier`, `name` | -| `product/variant/create` | `sku`, `name`, `language`, `productId` | -| `product/variant/update` | `language`, `sku` — **everything else optional** (`.partial()`) | -| `product/variant/upsert` | `sku`, `name`, `language`, `productId` | -| `product/variant/delete` | `sku` | -| `product/variant/stock/modify` | `sku`, `quantity`, `stockLocationIdentifier` | -| `product/variant/price/modify` | `sku`, `priceVariantIdentifier`, `price` | -| `customer/create`, `customer/upsert` | `identifier` | -| `customer/update` | per `UpdateCustomerInputSchema` | -| `customer/group/create`, `customer/group/update`, `customer/group/upsert` | per group input schema | -| `order/register` | `cart`, `customer` | -| `order/update` | `cart`, `customer`, + id — **runner requires `id` specifically** | -| `order/upsert` | `cart`, `customer` — **`pipelines` is omitted from this intent** | -| `subscription-contract/create` | `customerIdentifier`, `subscriptionPlan`, `status`, `item`, `recurring` | -| `subscription-contract/update` | as create, + id — **runner requires `id` specifically** | -| `subscription-contract/upsert` | as create | -| `pricelist/create`, `pricelist/upsert` | `identifier`, `name`, `priceVariants`, `selectedProductVariants`, `targetAudience` | -| `pricelist/update` | per `UpdatePriceListInputSchema` | -| `topic/create` | `name`, `language` | -| `topic/update`, `topic/upsert` | `name`, `language`, + id (`topicId` or `resourceIdentifier`) | -| `topic/delete` | + id | -| `flow/create`, `flow/upsert` | `name`, `stages`, `identifier` (`type` drives restriction mapping) | -| `flow/update` | `identifier` + per `UpdateFlowInputSchema` | -| `image/register` | `key` | -| `item/paths/addAliases`, `setAliases`, `removeAliases` | `language`, `paths`, + `itemId`/`resourceIdentifier` | -| `item/paths/addHistory`, `setHistory`, `removeHistory` | `language`, `paths`, + `itemId`/`resourceIdentifier` | -| `item/paths/addShortcuts`, `setShortcuts` | **`shortcuts`** (`[{parentId, position?}]`), + `itemId`/`resourceIdentifier` | -| `item/paths/removeShortcuts` | **`parentIds`**, + `itemId`/`resourceIdentifier` | +| Intent | Required fields | +| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `folder/create` | `name`, `shapeIdentifier`, `tree`, `language` | +| `folder/update` | as create, + id | +| `folder/upsert` | as create | +| `document/create` | `name`, `shapeIdentifier`, `tree`, `language` | +| `document/update` | as create, + id | +| `document/upsert` | as create | +| `product/create` | `name`, `shapeIdentifier`, `tree`, `vatTypeId`, `variants`, `language` | +| `product/update` | as create, + id | +| `product/upsert` | as create | +| `item/updateComponent/item` | `language`, `component`, + id — **runner requires `itemId` specifically** | +| `item/updateComponent/sku` | `language`, `component`, `sku` | +| `item/publish` | `language`, + id — **runner requires `itemId` specifically** | +| `item/unpublish` | schema-valid, **not implemented** — see above | +| `item/delete` | + id. Passing _both_ `itemId` and `resourceIdentifier` throws | +| `item/flow/stage/addItems` | `items` (min 1), `stageIdentifier`. Optional `moveFromFlowIdentifier`, `actionConfig` | +| `shape/create` | `identifier`, `name` | +| `shape/update`, `shape/upsert` | `identifier`, `name` | +| `piece/create`, `piece/update`, `piece/upsert` | `identifier`, `name` | +| `product/variant/create` | `sku`, `name`, `language`, `productId` | +| `product/variant/update` | `language`, `sku` — **everything else optional** (`.partial()`) | +| `product/variant/upsert` | `sku`, `name`, `language`, `productId` | +| `product/variant/delete` | `sku` | +| `product/variant/stock/modify` | `sku`, `quantity`, `stockLocationIdentifier` | +| `product/variant/price/modify` | `sku`, `priceVariantIdentifier`, `price` | +| `customer/create`, `customer/upsert` | `identifier` | +| `customer/update` | per `UpdateCustomerInputSchema` | +| `customer/group/create`, `customer/group/update`, `customer/group/upsert` | per group input schema | +| `order/register` | `cart`, `customer` | +| `order/update` | `cart`, `customer`, + id — **runner requires `id` specifically** | +| `order/upsert` | `cart`, `customer` — **`pipelines` is omitted from this intent** | +| `subscription-contract/create` | `customerIdentifier`, `subscriptionPlan`, `status`, `item`, `recurring` | +| `subscription-contract/update` | as create, + id — **runner requires `id` specifically** | +| `subscription-contract/upsert` | as create | +| `pricelist/create`, `pricelist/upsert` | `identifier`, `name`, `priceVariants`, `selectedProductVariants`, `targetAudience` | +| `pricelist/update` | per `UpdatePriceListInputSchema` | +| `topic/create` | `name`, `language` | +| `topic/update`, `topic/upsert` | `name`, `language`, + id (`topicId` or `resourceIdentifier`) | +| `topic/delete` | + id | +| `flow/create`, `flow/upsert` | `name`, `stages`, `identifier` (`type` drives restriction mapping) | +| `flow/update` | `identifier` + per `UpdateFlowInputSchema` | +| `image/register` | `key` | +| `item/paths/addAliases`, `setAliases`, `removeAliases` | `language`, `paths`, + `itemId`/`resourceIdentifier` | +| `item/paths/addHistory`, `setHistory`, `removeHistory` | `language`, `paths`, + `itemId`/`resourceIdentifier` | +| `item/paths/addShortcuts`, `setShortcuts` | **`shortcuts`** (`[{parentId, position?}]`), + `itemId`/`resourceIdentifier` | +| `item/paths/removeShortcuts` | **`parentIds`**, + `itemId`/`resourceIdentifier` | The three shortcut intents take `shortcuts`/`parentIds`, **not `paths`**, and they ignore any `language` you pass — the converter hardcodes the tenant default language for shortcut operations. @@ -96,23 +96,23 @@ you pass — the converter hardcodes the tenant default language for shortcut op The schema accepts `resourceIdentifier` far more widely than the runner implements it. Several converters carry a literal `ResourceIdentifier is not implemented yet` error. -| Intent | Behaviour when you supply only `resourceIdentifier` | -| --- | --- | -| `product\|document\|folder /update` | ✅ resolved via `loadByResourceIdentifier` (scoped by `language`) | -| `product\|document\|folder /upsert` | ✅ resolved; used to decide create-vs-update | -| `product\|document\|folder /create` | ✅ stored on the new item, so later ops can find it | -| `item/delete` | ✅ resolved. Supplying both it and `itemId` **throws** | -| `topic/update`, `topic/upsert` | ✅ resolved; mismatch against a supplied `topicId` throws | -| `topic/delete` | ✅ resolved (default language) | -| `topic/create` | ✅ stored on the new topic | -| `image/register` | ✅ used as a dedupe key — if already registered, the operation is skipped | -| `item/paths/*` | ✅ resolved; throws `Item with resourceIdentifier … not found` if missing | -| `item/updateComponent/item` | ❌ **throws** `itemId is required … ResourceIdentifier is not implemented yet` | -| `item/publish` | ❌ **throws** `Operation is missing itemId. ResourceIdentifier is not implemented yet` | -| `order/update` | ❌ **throws** `Operation is missing id. ResourceIdentifier is not implemented yet` | -| `order/upsert` | ❌ **silently registers a brand-new order** | -| `subscription-contract/update` | ❌ **throws** the same "not implemented" error | -| `subscription-contract/upsert` | ❌ **silently creates a new contract** | +| Intent | Behaviour when you supply only `resourceIdentifier` | +| ----------------------------------- | -------------------------------------------------------------------------------------- | +| `product\|document\|folder /update` | ✅ resolved via `loadByResourceIdentifier` (scoped by `language`) | +| `product\|document\|folder /upsert` | ✅ resolved; used to decide create-vs-update | +| `product\|document\|folder /create` | ✅ stored on the new item, so later ops can find it | +| `item/delete` | ✅ resolved. Supplying both it and `itemId` **throws** | +| `topic/update`, `topic/upsert` | ✅ resolved; mismatch against a supplied `topicId` throws | +| `topic/delete` | ✅ resolved (default language) | +| `topic/create` | ✅ stored on the new topic | +| `image/register` | ✅ used as a dedupe key — if already registered, the operation is skipped | +| `item/paths/*` | ✅ resolved; throws `Item with resourceIdentifier … not found` if missing | +| `item/updateComponent/item` | ❌ **throws** `itemId is required … ResourceIdentifier is not implemented yet` | +| `item/publish` | ❌ **throws** `Operation is missing itemId. ResourceIdentifier is not implemented yet` | +| `order/update` | ❌ **throws** `Operation is missing id. ResourceIdentifier is not implemented yet` | +| `order/upsert` | ❌ **silently registers a brand-new order** | +| `subscription-contract/update` | ❌ **throws** the same "not implemented" error | +| `subscription-contract/upsert` | ❌ **silently creates a new contract** | For the ❌ rows, resolve the ID yourself first — either with a `_ref` to the operation that created the entity, or with a `fetch*` helper (`references/templating.md`). @@ -121,22 +121,22 @@ entity, or with a `fetch*` helper (`references/templating.md`). An `upsert` is only idempotent if the converter has something to look the entity up by. -| Intent | Lookup key | Re-run safe? | -| --- | --- | --- | -| `piece/upsert` | `identifier` | ✅ | -| `shape/upsert` | `identifier` | ✅ | -| `customer/upsert` | `identifier` | ✅ | -| `customer/group/upsert` | `identifier` | ✅ | -| `pricelist/upsert` | `identifier` | ✅ | -| `flow/upsert` | `identifier` | ✅ | -| `product/variant/upsert` | `productId` + `sku` | ✅ | -| `topic/upsert` | `topicId` or `resourceIdentifier` (one is required) | ✅ | -| `product\|document\|folder /upsert` | `itemId` or `resourceIdentifier` — **only if you supply one** | ⚠️ | -| `order/upsert` | `id` only | ⚠️ | -| `subscription-contract/upsert` | `id` only | ⚠️ | +| Intent | Lookup key | Re-run safe? | +| ----------------------------------- | ------------------------------------------------------------- | ------------ | +| `piece/upsert` | `identifier` | ✅ | +| `shape/upsert` | `identifier` | ✅ | +| `customer/upsert` | `identifier` | ✅ | +| `customer/group/upsert` | `identifier` | ✅ | +| `pricelist/upsert` | `identifier` | ✅ | +| `flow/upsert` | `identifier` | ✅ | +| `product/variant/upsert` | `productId` + `sku` | ✅ | +| `topic/upsert` | `topicId` or `resourceIdentifier` (one is required) | ✅ | +| `product\|document\|folder /upsert` | `itemId` or `resourceIdentifier` — **only if you supply one** | ⚠️ | +| `order/upsert` | `id` only | ⚠️ | +| `subscription-contract/upsert` | `id` only | ⚠️ | The ⚠️ rows are the trap. With neither key present the runner short-circuits straight to a create, every run. -`externalReference` is *not* consulted. A `product/upsert` keyed only on `externalReference` creates a +`externalReference` is _not_ consulted. A `product/upsert` keyed only on `externalReference` creates a duplicate product on every run. The item lookup is also **language-scoped**, so upserting the same item under a second language without an `itemId` will create a second item. @@ -147,17 +147,17 @@ in `OperationLog.output`, which is the authoritative view. **Verified** (at `version: "1.0.0"`): -| Intent | `_ref` output | -| --- | --- | -| `product/upsert`, `folder/upsert`, `document/upsert` | `{ id: string }` — flat, in all branches | -| `shape/upsert` | `{ identifier: string }` | -| `piece/create`, `piece/upsert` | `{ identifier: string }` | -| `customer/create`, `customer/update`, `customer/upsert` | `{ identifier: string }` | -| `product/variant/create`, `/update`, `/upsert` | the **full variant DTO** minus `tenantId`/`language` (id, sku, name, isDefault, priceVariants, stockLocations, components, …); falls back to `{ sku }` if the variant can't be resolved | -| `product/variant/delete` | `{ sku: string }` | -| `item/publish` | `{ language, success: string[], failure: [{ itemId, error }] }` | -| `item/updateComponent/item`, `item/updateComponent/sku` | a **bare ID string**, not `{ id }` — reference it as `{{ myRef }}` | -| `product/variant/stock/modify` | a **bare ID string**, same as above | +| Intent | `_ref` output | +| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `product/upsert`, `folder/upsert`, `document/upsert` | `{ id: string }` — flat, in all branches | +| `shape/upsert` | `{ identifier: string }` | +| `piece/create`, `piece/upsert` | `{ identifier: string }` | +| `customer/create`, `customer/update`, `customer/upsert` | `{ identifier: string }` | +| `product/variant/create`, `/update`, `/upsert` | the **full variant DTO** minus `tenantId`/`language` (id, sku, name, isDefault, priceVariants, stockLocations, components, …); falls back to `{ sku }` if the variant can't be resolved | +| `product/variant/delete` | `{ sku: string }` | +| `item/publish` | `{ language, success: string[], failure: [{ itemId, error }] }` | +| `item/updateComponent/item`, `item/updateComponent/sku` | a **bare ID string**, not `{ id }` — reference it as `{{ myRef }}` | +| `product/variant/stock/modify` | a **bare ID string**, same as above | **Not verified.** Every other intent returns whatever its underlying operation produces, and the shape varies — several return nothing at all, which normalises to `null`. The public docs' table for these @@ -196,7 +196,7 @@ counting the whole subtree; **50 items** per `item/flow/stage/addItems`; **500 c ## Schema quirks worth knowing - **Upsert = create schema.** `UpsertProductOperationSchema` extends `CreateProductInputSchema`, not the - update one. So an upsert needs the full create payload — it is *not* a sparse patch. Same for + update one. So an upsert needs the full create payload — it is _not_ a sparse patch. Same for document, folder, piece, shape, customer, customer group, pricelist, topic, flow, variant, and subscription contract. - **`piece/update` also uses `CreatePieceInputSchema`**, so it too requires the full payload — and the @@ -213,7 +213,7 @@ counting the whole subtree; **50 items** per `item/flow/stage/addItems`; **500 c `product/create|update|upsert` and `product/variant/create|update|upsert`. So tiered pricing works even though the fields don't appear in the schema. Matching is by `identifier`, falling back to array position only when no raw entry has one. -- **`variant.topicIds` is silently dropped.** Also stripped by the schema, and *not* re-attached — +- **`variant.topicIds` is silently dropped.** Also stripped by the schema, and _not_ re-attached — there is an explicit `blocked on @crystallize/schema bump` comment in both variant converters. Assign variant topics another way. - **`item/updateComponent/item` runs with `disableContentValidation: true`.** Component content is @@ -226,44 +226,44 @@ counting the whole subtree; **50 items** per `item/flow/stage/addItems`; **500 c ```json { - "version": "1.0.0", - "operations": [ - { - "intent": "customer/upsert", - "identifier": "customer-for-order-123", - "firstName": "John", - "lastName": "Doe", - "type": "individual" - }, - { - "intent": "order/register", - "customer": { "identifier": "customer-for-order-123", "type": "individual" }, - "additionalInformation": "Please deliver between 9am-5pm", - "cart": [ + "version": "1.0.0", + "operations": [ { - "sku": "SP-RED-001", - "name": "Sample Product", - "productId": "67e5d2d12d31ee752710a74b", - "quantity": 2, - "price": { - "currency": "USD", - "gross": 1000, - "net": 800, - "tax": { "name": "VAT", "percent": 20 } - } + "intent": "customer/upsert", + "identifier": "customer-for-order-123", + "firstName": "John", + "lastName": "Doe", + "type": "individual" + }, + { + "intent": "order/register", + "customer": { "identifier": "customer-for-order-123", "type": "individual" }, + "additionalInformation": "Please deliver between 9am-5pm", + "cart": [ + { + "sku": "SP-RED-001", + "name": "Sample Product", + "productId": "67e5d2d12d31ee752710a74b", + "quantity": 2, + "price": { + "currency": "USD", + "gross": 1000, + "net": 800, + "tax": { "name": "VAT", "percent": 20 } + } + } + ] + }, + { + "intent": "item/updateComponent/item", + "itemId": "632958a35dfc2c90cbbad20d", + "language": "en", + "component": { + "componentId": "title", + "singleLine": { "text": "Mass operation updated title" } + } } - ] - }, - { - "intent": "item/updateComponent/item", - "itemId": "632958a35dfc2c90cbbad20d", - "language": "en", - "component": { - "componentId": "title", - "singleLine": { "text": "Mass operation updated title" } - } - } - ] + ] } ``` @@ -278,30 +278,30 @@ it duplicates the product on every run. Corrected: ```json { - "version": "1.0.0", - "operations": [ - { - "_ref": "rootProducts", - "intent": "product/upsert", - "resourceIdentifier": "product-sku-12345", - "externalReference": "SKU-12345", - "name": "My Product", - "language": "en", - "shapeIdentifier": "product", - "tree": { "parentId": "{{ defaults.rootItemId }}" }, - "vatTypeId": "{{ defaults.vatTypeIds.[0] }}", - "variants": [{ "sku": "SKU-12345", "name": "My Product", "isDefault": true }] - }, - { - "intent": "item/updateComponent/item", - "itemId": "{{ rootProducts.id }}", - "language": "en", - "component": { - "componentId": "description", - "richText": { "html": ["
Updated description
"] } - } - } - ] + "version": "1.0.0", + "operations": [ + { + "_ref": "rootProducts", + "intent": "product/upsert", + "resourceIdentifier": "product-sku-12345", + "externalReference": "SKU-12345", + "name": "My Product", + "language": "en", + "shapeIdentifier": "product", + "tree": { "parentId": "{{ defaults.rootItemId }}" }, + "vatTypeId": "{{ defaults.vatTypeIds.[0] }}", + "variants": [{ "sku": "SKU-12345", "name": "My Product", "isDefault": true }] + }, + { + "intent": "item/updateComponent/item", + "itemId": "{{ rootProducts.id }}", + "language": "en", + "component": { + "componentId": "description", + "richText": { "html": ["Updated description
"] } + } + } + ] } ``` @@ -315,17 +315,15 @@ The published version omits `name` on the component. Every component definition ```json { - "version": "1.0.0", - "operations": [ - { - "intent": "piece/upsert", - "identifier": "rating-system", - "name": "Rating System", - "components": [ - { "id": "name", "name": "Name", "type": "singleLine", "singleLine": { "required": true } } - ] - } - ] + "version": "1.0.0", + "operations": [ + { + "intent": "piece/upsert", + "identifier": "rating-system", + "name": "Rating System", + "components": [{ "id": "name", "name": "Name", "type": "singleLine", "singleLine": { "required": true } }] + } + ] } ``` @@ -337,7 +335,7 @@ does. The skill follows the runner. ### Examples that fail schema validation 1. `piece/upsert` example omits `name` on the component definition. -2. `product/upsert` example omits `tree`, `vatTypeId` and `variants` (upserts extend the *create* +2. `product/upsert` example omits `tree`, `vatTypeId` and `variants` (upserts extend the _create_ schema). 3. `richText.html` is passed as a string; the schema requires an array of strings. @@ -355,7 +353,7 @@ does. The skill follows the runner. `subscription-contract/update|upsert`. 8. `item/unpublish` is documented as a usable intent; it has no converter and aborts the whole task. 9. `item/paths/*Shortcuts` are documented as taking `paths`; they take `shortcuts` / `parentIds`. -10. `item/paths/set*` is presented as a set operation; it removes *or* adds in a single run, never both. +10. `item/paths/set*` is presented as a set operation; it removes _or_ adds in a single run, never both. 11. `version` is presented as a version label. It selects the converter set, and three of the four formats the regex accepts crash the runner. 12. `externalReference` is used as the de-duplication key in the flagship `product/upsert` example, diff --git a/use-crystallize/skills/mass-operations/references/lifecycle.md b/use-crystallize/skills/mass-operations/references/lifecycle.md index 533330c..a0d88ee 100644 --- a/use-crystallize/skills/mass-operations/references/lifecycle.md +++ b/use-crystallize/skills/mass-operations/references/lifecycle.md @@ -20,18 +20,21 @@ that detail away (see Troubleshooting). ``` crystallize mass-operation dump-content-model