Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/skills-definehook-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
---

docs-only: skills/objectstack-data hook examples author with `defineHook()`
instead of bare `: Hook` literals, closing the #4269 authoring-validation loop
on the skill surface (#4274). Releases nothing.
10 changes: 4 additions & 6 deletions skills/objectstack-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,9 @@ Implement business logic at data operation lifecycle points:

<!-- os:check -->
```typescript
import { Hook, HookContext } from '@objectstack/spec/data';
import { defineHook, HookContext } from '@objectstack/spec/data';

const accountHook: Hook = {
export default defineHook({
name: 'account_defaults',
object: 'account',
events: ['beforeInsert'],
Expand All @@ -408,9 +408,7 @@ const accountHook: Hook = {
}
ctx.input.created_at = new Date().toISOString();
},
};

export default accountHook;
});
```

The `handler` above is the inline (in-process) form. The **preferred**,
Expand All @@ -433,7 +431,7 @@ Mirror these CRM-style patterns when designing enterprise metadata objects:
| Capability gating | `src/objects/*.object.ts` | Use `enable` flags (`trackHistory`, `apiMethods`, `files`, `feeds`, `activities`) per object |
| Index + validation pairing | `src/objects/*.object.ts` | Keep `indexes[]` aligned to common filters and enforce invariants with `validations[]` |
| Relationship constraints | `src/objects/*.object.ts` | Use `lookup` + `lookupFilters` (`[{ field, operator, value }]`) for constrained child selection |
| Lifecycle automation | `src/objects/*.hook.ts` | Use a lifecycle **hook** (a `Hook` object registered via `defineStack({ hooks })`) or a top-level `record_change` flow for field updates triggered by record changes. There is **no** object-level `workflows[]` field — authoring one is a build error (#1535). |
| Lifecycle automation | `src/objects/*.hook.ts` | Use a lifecycle **hook** (authored with `defineHook()`, registered via `defineStack({ hooks })` or the `*.hook.ts` convention scan) or a top-level `record_change` flow for field updates triggered by record changes. There is **no** object-level `workflows[]` field — authoring one is a build error (#1535). |
| State transitions | `src/objects/*.object.ts` | Prefer explicit `state_machine` validation rules (one per state field) — there is **no** separate `stateMachines` map |

For metadata authoring, keep expressions in CEL (`P\`...\``, `F\`...\``,
Expand Down
73 changes: 39 additions & 34 deletions skills/objectstack-data/references/data-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,18 @@ ObjectStack provides **8 lifecycle events** organized by operation type:

## Hook Definition Schema

Every hook must conform to the `HookSchema`:
Every hook must conform to the `HookSchema`. Author it with `defineHook()` —
preferred over a bare `: Hook` literal (the same rule as `defineDatasource`):
it validates when the module is imported, so constraint-level mistakes a bare
annotation can't catch — a non-`snake_case` `name`, a misspelled key routed
through a spread — fail while you author instead of at deploy, and the export
carries defaults already materialized (#4269).

```typescript
import { P } from '@objectstack/spec';
import { Hook, HookContext } from '@objectstack/spec/data';
import { defineHook, HookContext } from '@objectstack/spec/data';

const myHook: Hook = {
const myHook = defineHook({
// Required: Unique identifier (snake_case)
name: 'my_validation_hook',

Expand Down Expand Up @@ -122,7 +127,7 @@ const myHook: Hook = {
maxRetries: 3,
backoffMs: 1000,
},
};
});
```

### Key Properties Explained
Expand Down Expand Up @@ -420,9 +425,9 @@ is **not** in the partial patch) and flip that position to `filled`:

<!-- os:check -->
```typescript
import type { Hook } from '@objectstack/spec/data';
import { defineHook } from '@objectstack/spec/data';

const fillPositionOnHire: Hook = {
const fillPositionOnHire = defineHook({
name: 'fill_position_on_hire',
object: 'candidate',
events: ['afterUpdate'],
Expand All @@ -441,7 +446,7 @@ const fillPositionOnHire: Hook = {
capabilities: ['api.read', 'api.write', 'log'],
},
onError: 'log',
};
});

export default fillPositionOnHire;
```
Expand Down Expand Up @@ -675,7 +680,7 @@ handler: async (ctx: HookContext) => {
### 1. Setting Default Values

```typescript
const setAccountDefaults: Hook = {
const setAccountDefaults = defineHook({
name: 'account_defaults',
object: 'account',
events: ['beforeInsert'],
Expand All @@ -693,13 +698,13 @@ const setAccountDefaults: Hook = {
ctx.input.owner_id = ctx.session.userId;
}
},
};
});
```

### 2. Data Validation

```typescript
const validateAccount: Hook = {
const validateAccount = defineHook({
name: 'account_validation',
object: 'account',
events: ['beforeInsert', 'beforeUpdate'],
Expand All @@ -719,13 +724,13 @@ const validateAccount: Hook = {
throw new Error('Annual revenue cannot be negative');
}
},
};
});
```

### 3. Preventing Deletion

```typescript
const protectStrategicAccounts: Hook = {
const protectStrategicAccounts = defineHook({
name: 'protect_strategic_accounts',
object: 'account',
events: ['beforeDelete'],
Expand All @@ -747,13 +752,13 @@ const protectStrategicAccounts: Hook = {
throw new Error(`Cannot delete account with ${oppCount} active opportunities`);
}
},
};
});
```

### 4. Data Enrichment

```typescript
const enrichLeadScore: Hook = {
const enrichLeadScore = defineHook({
name: 'lead_scoring',
object: 'lead',
events: ['beforeInsert', 'beforeUpdate'],
Expand Down Expand Up @@ -782,13 +787,13 @@ const enrichLeadScore: Hook = {

ctx.input.score = score;
},
};
});
```

### 5. Triggering Workflows

```typescript
const notifyOnStatusChange: Hook = {
const notifyOnStatusChange = defineHook({
name: 'notify_status_change',
object: 'opportunity',
events: ['afterUpdate'],
Expand All @@ -810,13 +815,13 @@ const notifyOnStatusChange: Hook = {
// });
}
},
};
});
```

### 6. Creating Related Records

```typescript
const createAuditTrail: Hook = {
const createAuditTrail = defineHook({
name: 'audit_trail',
object: ['account', 'contact', 'opportunity'],
events: ['afterInsert', 'afterUpdate', 'afterDelete'],
Expand All @@ -836,13 +841,13 @@ const createAuditTrail: Hook = {
} : undefined,
});
},
};
});
```

### 7. External API Integration

```typescript
const syncToExternalCRM: Hook = {
const syncToExternalCRM = defineHook({
name: 'sync_external_crm',
object: 'account',
events: ['afterInsert', 'afterUpdate'],
Expand All @@ -867,13 +872,13 @@ const syncToExternalCRM: Hook = {
console.error('Failed to sync to external CRM', error);
}
},
};
});
```

### 8. Multi-Object Logic

```typescript
const cascadeAccountUpdate: Hook = {
const cascadeAccountUpdate = defineHook({
name: 'cascade_account_updates',
object: 'account',
events: ['afterUpdate'],
Expand All @@ -887,13 +892,13 @@ const cascadeAccountUpdate: Hook = {
);
}
},
};
});
```

### 9. Conditional Execution

```typescript
const highValueAccountAlert: Hook = {
const highValueAccountAlert = defineHook({
name: 'high_value_alert',
object: 'account',
events: ['afterInsert'],
Expand All @@ -904,7 +909,7 @@ const highValueAccountAlert: Hook = {
console.log(`🚨 High-value account created: ${ctx.result.name}`);
// Send alert to sales leadership
},
};
});
```

### 10. Data Masking (Read Operations)
Expand All @@ -916,7 +921,7 @@ const highValueAccountAlert: Hook = {
> subscription covers both `find` and `findOne`.

```typescript
const maskSensitiveData: Hook = {
const maskSensitiveData = defineHook({
name: 'mask_pii',
object: ['contact', 'lead'],
events: ['afterFind'], // fires for findOne too — no separate afterFindOne
Expand All @@ -942,7 +947,7 @@ const maskSensitiveData: Hook = {
}
}
},
};
});
```

---
Expand Down Expand Up @@ -1013,16 +1018,16 @@ export const onEnable = async (ctx: { ql: ObjectQL }) => {

```typescript
// src/objects/account.hook.ts
import { Hook, HookContext } from '@objectstack/spec/data';
import { defineHook, HookContext } from '@objectstack/spec/data';

const accountHook: Hook = {
const accountHook = defineHook({
name: 'account_logic',
object: 'account',
events: ['beforeInsert', 'beforeUpdate'],
handler: async (ctx: HookContext) => {
// Validation logic
},
};
});

export default accountHook;

Expand Down Expand Up @@ -1250,7 +1255,7 @@ const validators = [
validateWebsite,
];

const composedHook: Hook = {
const composedHook = defineHook({
name: 'validation_suite',
object: 'account',
events: ['beforeInsert', 'beforeUpdate'],
Expand All @@ -1259,13 +1264,13 @@ const composedHook: Hook = {
await validator(ctx);
}
},
};
});
```

### Conditional Hook Execution

```typescript
const conditionalHook: Hook = {
const conditionalHook = defineHook({
name: 'enterprise_only',
object: 'account',
events: ['afterInsert'],
Expand All @@ -1277,7 +1282,7 @@ const conditionalHook: Hook = {

// Enterprise-specific logic
},
};
});
```

---
Expand Down
13 changes: 10 additions & 3 deletions skills/objectstack-data/rules/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ The canonical reference includes:

```typescript
import { P } from '@objectstack/spec';
import { Hook, HookContext } from '@objectstack/spec/data';
import { defineHook, HookContext } from '@objectstack/spec/data';

const hook: Hook = {
const hook = defineHook({
name: 'my_hook', // Required: unique identifier
object: 'account', // Required: target object(s)
events: ['beforeInsert'], // Required: lifecycle events
Expand All @@ -40,9 +40,16 @@ const hook: Hook = {
priority: 100, // Optional: execution order
async: false, // Optional: background execution (after* only)
condition: P`record.status == 'active'`, // Optional: conditional execution (CEL)
};
});
```

Prefer `defineHook()` over a bare `: Hook` literal (the same rule as
`defineDatasource`): it validates when the module is imported, so
constraint-level mistakes a bare annotation can't catch — a non-`snake_case`
`name`, a misspelled key routed through a spread — fail while you author
instead of at deploy, and the export carries defaults already materialized
(#4269).

### Logic: `body` (preferred) or `handler` (deprecated)

A hook's logic comes from **either** an inline `handler` function **or** a
Expand Down
6 changes: 3 additions & 3 deletions skills/objectstack-data/rules/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,9 @@ Calling an external API, hitting another object, or running arbitrary code is
lifecycle hook and throw on failure — the typed, supported extension point:

```typescript
import { Hook, HookContext } from '@objectstack/spec/data';
import { defineHook, HookContext } from '@objectstack/spec/data';

const taxIdCheck: Hook = {
const taxIdCheck = defineHook({
name: 'tax_id_external_check',
object: 'account',
events: ['beforeInsert', 'beforeUpdate'],
Expand All @@ -246,7 +246,7 @@ const taxIdCheck: Hook = {
throw new Error('Invalid tax ID');
}
},
};
});
```

## Validation Properties
Expand Down
Loading