# Command Implementation

## Unified Pattern: `run` + generated shell

All commands follow the same pattern — export a `run` function, and the generated shell wraps it with `defineCommand`:

**Implementation file** (`command/myCommand.ts`):

```typescript
export async function run(db: DB, input: MyCommandInput, ctx: CommandContext) {
  // validate → query → mutate
  return ok({ entity });
}
```

**Generated shell** (`command/myCommand.generated.ts`):

```typescript
export const myCommand = defineCommand(permissions.myCommand, run);
```

## Custom Fields (generic CF)

Commands that **write** (insert or update) into a table with user-extensible fields make `run` generic:

- Generic `CF extends Record<string, unknown>` on the `run` function
- Input type: `CreateXInput & CF` or `UpdateXInput & Partial<CF>`
- Destructure known fields, rest-spread custom fields
- Cast custom fields: `...(customFields as Record<string, unknown>)`

```typescript
export async function run<CF extends Record<string, unknown>>(
  db: DB,
  input: CreateXInput & CF,
  ctx: CommandContext,
) {
  const { name, ...customFields } = input;
  await db
    .insertInto("X")
    .values({ ...(customFields as Record<string, unknown>), name })
    .execute();
}
```

### module.ts wiring

Use instantiation expressions to lock `CF` to the module's custom field type:

```typescript
const createXTyped = createX<FieldsToInsertable<F>>;
return { commands: { createX: createXTyped } };
```

### Rule: when to use generic CF

> If a model uses `fields?: F` for custom fields, **every command that writes those fields** must have a generic `CF`. This includes create, update, and any other write command. The determining factor is whether the command writes to a table with custom fields, not just whether it inserts new rows.

## Config-Taking Commands

Commands that need external config (cross-module queries, settings) accept config as leading params before `(db, input, ctx)`:

```typescript
export async function run<CF extends Record<string, unknown>>(
  primitivesQueries: Pick<PrimitivesQueries, "getUnit">,
  db: DB, input: CreateItemInput & CF, ctx: CommandContext,
) { ... }
```

module.ts wires with `.bind()` after instantiation:

```typescript
const createItemTyped = createItem<FieldsToInsertable<IF>>;
return { commands: { createItem: createItemTyped.bind(null, queries) } };
```

## Command-Side Reads (CQRS Separation)

Following CQRS principles, commands own their reads for same-module data. Cross-module reads use injected query functions to preserve module boundaries.

### Why

- **Query functions** use `ReadonlyDB` — shaped for API consumers, can't `forUpdate()`
- **Command reads** are shaped for enforcing invariants — need locking, may need different filters or joins
- **Independence** — query-side changes (pagination, field selection) must not affect command behavior
- **Module composition** — modules nest at multiple depths; parent modules inject child queries to maintain explicit dependency contracts

### Rule

> **Same-module reads**: Commands inline their own `selectFrom()` calls using `db` (full `DB`). Never import or call query functions from `query/` within the same module — the command owns its reads and can apply locking.
>
> **Cross-module reads**: Commands receive query functions from other modules via dependency injection (leading params). This keeps module boundaries explicit and supports nested module composition.
>
> **Status preconditions**: Commands enforce status checks inline (e.g., "only DRAFT items can be deleted", "only ACTIVE users can be assigned roles"). Status filtering belongs to the command's invariant logic, not to query-side defaults. See [Status-Aware Query Rules](queries.md#status-aware-query-rules) for the read-side conventions.

### Examples

**Correct** — same-module read inlined with locking:

```typescript
// command/deactivateUnit.ts
const unit = await db
  .selectFrom("Unit")
  .selectAll()
  .where("id", "=", input.unitId)
  .forUpdate()
  .executeTakeFirst();
if (!unit) return err(new UnitNotFoundError(input.unitId));
```

**Correct** — cross-module read via injected query:

```typescript
// command/createItem.ts (item-management validating a primitives entity)
export async function run<CF extends Record<string, unknown>>(
  primitivesQueries: Pick<PrimitivesQueries, "getUnit">,
  db: DB,
  input: CreateItemInput & CF,
  ctx: CommandContext,
) {
  const { unit } = await primitivesQueries.getUnit(db, { id: input.unitId }, ctx);
  if (!unit?.isActive) return err(new UnitNotFoundError(input.unitId));
  // ...
}
```

**Wrong** — calling same-module query function:

```typescript
// command/deactivateUnit.ts
import { getUnit } from "../query/getUnit"; // ← don't do this
const unit = await getUnit(db, { id: input.unitId }, ctx);
```

## Select Locking

Commands that read then write must lock rows with `forUpdate()` to prevent concurrent requests from causing race conditions.

### When to lock

| Pattern                                                     | Lock? | Example                                                                               |
| ----------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------- |
| Read a record, then update/delete it                        | Yes   | `deactivateUnit`: reads unit, then sets `isActive = false`                            |
| Read a record to check uniqueness before insert             | Yes   | `createUnit`: checks symbol uniqueness, then inserts                                  |
| Read multiple related records, then update them             | Yes   | `setReferenceUnit`: reads all units in category, recalculates factors                 |
| Read a record only for validation (no write to that record) | No    | Reading a category to verify it exists before inserting a unit into a different table |
| Insert-only with no prior read                              | No    | `logAuditEvent`: pure insert                                                          |

### Rule

> Lock a `SELECT` when the command **writes to the same record it reads**, or when it **reads to enforce a uniqueness constraint** before inserting. The lock scope should be the narrowest set of rows needed — lock only the rows that participate in the read-then-write cycle.

### Code pattern

Chain `.forUpdate()` on the select query:

```typescript
const unit = await db
  .selectFrom("Unit")
  .selectAll()
  .where("id", "=", input.unitId)
  .forUpdate()
  .executeTakeFirst();
```

### Batch locking

When a command reads multiple rows then updates them (e.g., `setReferenceUnit`), lock the entire set:

```typescript
const units = await db
  .selectFrom("Unit")
  .selectAll()
  .where("categoryId", "=", input.categoryId)
  .forUpdate()
  .execute();
```

## Implementation Considerations

- **Error handling**: Use `ok()` / `err()` from `@tailor-platform/erp-kit/module` — do not throw
- **Validation**: Check referenced entities exist before operating, return `err()` if not found
- **Return property naming**: Use the full model name in camelCase as the return property key — never abbreviate. This keeps property names unambiguous across modules.
  - `return ok({ stockMovement: ... })` — not `movement`
  - `return ok({ inventoryAdjustment: ... })` — not `adjustment`

## Update Command Input Type

Update commands use a structured type that separates lookup keys from mutable fields, and explicitly excludes status-controlled fields.

### Input type shape

```typescript
export type UpdateXInput = ({
  id: string;
} | {
  naturalKey: string; // e.g. sku, code
}) & {
  mutableField1?: string;
  mutableField2?: string | null;
}
```

### Run signature with custom fields

```typescript
export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: UpdateXInput & Omit<Partial<CF>, "status">,
  ctx: CommandContext,
)
```

### Rules

| Field kind | Examples | How to handle |
|---|---|---|
| Lookup key (primary) | `id` | One branch of the union |
| Lookup key (natural) | `sku`, `code` | Another branch of the union — used to look up the record, not to update it |
| Mutable fields | `name`, `barcode` | Listed in the `&` intersection as optional fields |
| Status-controlled | `status` | **Never in the input type.** Managed by dedicated commands (`activateX`, `deactivateX`). Excluded from `CF` via `Omit`. |
| System fields | `createdAt`, `updatedAt` | Never in the input type. Set automatically. |

### Lookup implementation

Use `"id" in input` to branch between lookup strategies:

```typescript
let query = db.selectFrom("Item").selectAll();
if ("id" in input) {
  query = query.where("id", "=", input.id);
} else {
  query = query.where("sku", "=", (input as { sku: string }).sku);
}
const item = await query.forUpdate().executeTakeFirst();
```

### Custom field extraction

Destructure known fields and treat the rest as custom fields:

```typescript
const { name, barcode, unitId, ...rest } = input as any;
const { id: _id, sku: _sku, ...customFields } = rest;
```

### Why not throw immutable errors

Natural keys like `sku` or `code` are **lookup keys**, not updatable fields. They do not belong in the mutable section of the input type — the type system prevents callers from passing them as update targets. There is no need for a `SkuImmutableError` or `CodeImmutableError` in an update command.

> Exception: conditionally-mutable fields (e.g., `baseCurrencyId` that can only change in DRAFT status) belong in the mutable section and use a business-rule error when the condition is not met.

## Conventions

- Input types for create commands: exported interfaces (`export interface CreateXInput`)
- Input types for update commands: exported type aliases (`export type UpdateXInput = ...`)
- Use `.executeTakeFirst()` for single results
- Include JSDoc: `/** Function: name \n Description */`

## State Transitions

For commands that transition between statuses, accept `from?: string[]` with a default:

```typescript
from?: string[];  // Default: ["ACTIVE"]

const validFromStatuses = input.from ?? ["ACTIVE"];
if (!validFromStatuses.includes(user.status)) {
  return err(new InvalidStatusTransitionError(user.status, targetStatus));
}
```

- Default `from` contains the base valid source status
- Parent modules can override to allow transitions from additional statuses
