# Resolver & Executor Patterns

## Resolver Patterns

All resolvers export a default `createResolver()` from `@tailor-platform/sdk`.

### Verify command input types

Before implementing a resolver, read the command's type definition:

```
node_modules/@tailor-platform/erp-kit/src/modules/<module>/command/<commandName>.ts
```

The command source is the ground truth. Do not invent fields that don't exist, and preserve the required/optional distinction.

### SDK type system

See [Resolver docs](https://raw.githubusercontent.com/tailor-platform/sdk/refs/heads/main/packages/sdk/docs/services/resolver.md) for the `t` namespace.

`t.array()` and `t.boolean()` do **not exist**. Never attempt to use them.

Use `t.uuid()` for fields that hold UUIDs (e.g. entity IDs, foreign keys). Do not use `t.string()` for UUID fields.

### Command-based mutation

The most common pattern. Key points:

```ts
import { createContext } from "@tailor-platform/erp-kit/app";

body: async (context) => {
  const db = getDB("main-db");
  const result = await imCommands.createItem(db, { ...context.input }, createContext(context));
  // error handling — see below
  if (!result.ok) { /* switch on result.error.code */ }
  return { id: result.value.item.id, ... };
},
```

- `context.input.optionalField ?? undefined` — Convert null to undefined for command inputs
- `result.value.entity.nullableField ?? ""` — Handle nullable return values
- `createContext(context)` wraps the resolver context for erp-kit commands
- `getDB("main-db")` — Namespace must match `tailor.config.ts`
- Do not directly mutate module-owned tables via Kysely — always use module commands

### Document update mutations (headerPatch + line collections)

Document update/amend commands take `{ id, headerPatch?, addLines?, updateLines?, removeLineIds? }` — three explicit line collections, each fully typed on its own:

```ts
input: {
  id: t.uuid(),
  headerPatch: t.object({ /* patchable header fields, all optional */ }, { optional: true }),
  addLines: t.object({ /* full line input */ }, { optional: true, array: true }),
  updateLines: t.object({
    lineId: t.uuid(),
    linePatch: t.object({ /* patchable line fields, all optional */ }),
  }, { optional: true, array: true }),
  removeLineIds: t.uuid({ optional: true, array: true }),
},
```

- Pass the input through to the command as-is.
- **Return `{ id }`, typed as the document entity** via `typeName` (reference the db type so the name can't drift):

```ts
import { purchaseOrder } from "@/db"; // the db.type() instance

body: async (context) => {
  const db = getDB("main-db");
  return db.transaction().execute(async (trx) => {
    const result = await imCommands.updatePurchaseOrder(trx, { ...context.input }, createContext(context));
    if (!result.ok) { /* switch on result.error.code */ }
    return { id: result.value.purchaseOrder.id }; // key only
  });
},
output: t.object({ id: t.uuid() }).typeName(purchaseOrder.name),
```

  `typeName` keys the result to the entity, which buys two things: the client selects header fields and the paginated `lines` straight off the mutation result (federation resolves them off the entity, same path as a query), and the `__typename`+`id` key lets a normalized GraphQL cache merge the result so existing views refresh.

### Error handling

Resolver specs document error codes the command can return. Add a `case` for every documented code and a `default` with `satisfies never` for exhaustiveness.

**Do this:**

```ts
if (!result.ok) {
  switch (result.error.code) {
    case "INSUFFICIENT_STOCK":
      throw new Error(`Stock insufficient for item ${context.input.itemId}`);
    case "ORDER_ALREADY_CONFIRMED":
      throw new Error("Cannot modify a confirmed order");
    default:
      throw result.error satisfies never;
  }
}
```

**Not this:**

```ts
if (!result.ok) throw result.error;
```
