Add objectstack-hooks skill guide for third-party developers#1119
Add objectstack-hooks skill guide for third-party developers#1119
Conversation
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/65fb3af8-127a-49b6-a200-29a63b6a8073 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a new objectstack-hooks skill to document how third-party developers can implement ObjectStack data lifecycle hooks, including schema references intended to mirror @objectstack/spec.
Changes:
- Added a comprehensive hooks skill guide (
skills/objectstack-hooks/SKILL.md) with lifecycle event docs, patterns, and example code. - Added a schema reference snapshot for hooks (
skills/objectstack-hooks/references/data/hook.zod.ts). - Added a references index (
skills/objectstack-hooks/references/_index.md) for navigating the snapshots.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| skills/objectstack-hooks/SKILL.md | New end-to-end guide for authoring and registering hooks, plus patterns and testing guidance. |
| skills/objectstack-hooks/references/data/hook.zod.ts | Local “snapshot” of the hook Zod schemas for quick reference. |
| skills/objectstack-hooks/references/_index.md | Index page describing the available reference schema snapshots. |
| ObjectStack provides **14 lifecycle events** organized by operation type: | ||
|
|
||
| | Event | When It Fires | Use Cases | | ||
| |:------|:--------------|:----------| | ||
| | **Read Operations** | | | | ||
| | `beforeFind` | Before querying multiple records | Filter queries by user context, log access | | ||
| | `afterFind` | After querying multiple records | Transform results, mask sensitive data | | ||
| | `beforeFindOne` | Before fetching a single record | Validate permissions, log access | | ||
| | `afterFindOne` | After fetching a single record | Enrich data, mask fields | | ||
| | `beforeCount` | Before counting records | Filter by context | | ||
| | `afterCount` | After counting records | Log metrics | | ||
| | `beforeAggregate` | Before aggregate operations | Validate aggregation rules | | ||
| | `afterAggregate` | After aggregate operations | Transform results | | ||
| | **Write Operations** | | | | ||
| | `beforeInsert` | Before creating a record | Set defaults, validate, normalize | |
There was a problem hiding this comment.
The guide states ObjectStack provides “14 lifecycle events”, but the canonical HookEvent enum includes additional bulk events (before/afterUpdateMany, before/afterDeleteMany) in @objectstack/spec, and the current ObjectQL engine only triggers a smaller subset (before/afterFind/Insert/Update/Delete). Please update this section to either (a) list all events from HookEvent and clearly mark which are currently emitted by the engine, or (b) scope the guide explicitly to the events that are actually triggered today.
| ObjectStack provides **14 lifecycle events** organized by operation type: | |
| | Event | When It Fires | Use Cases | | |
| |:------|:--------------|:----------| | |
| | **Read Operations** | | | | |
| | `beforeFind` | Before querying multiple records | Filter queries by user context, log access | | |
| | `afterFind` | After querying multiple records | Transform results, mask sensitive data | | |
| | `beforeFindOne` | Before fetching a single record | Validate permissions, log access | | |
| | `afterFindOne` | After fetching a single record | Enrich data, mask fields | | |
| | `beforeCount` | Before counting records | Filter by context | | |
| | `afterCount` | After counting records | Log metrics | | |
| | `beforeAggregate` | Before aggregate operations | Validate aggregation rules | | |
| | `afterAggregate` | After aggregate operations | Transform results | | |
| | **Write Operations** | | | | |
| | `beforeInsert` | Before creating a record | Set defaults, validate, normalize | | |
| The canonical `HookEvent` enum in `@objectstack/spec` defines additional lifecycle | |
| events, including bulk operations such as `beforeUpdateMany`, `afterUpdateMany`, | |
| `beforeDeleteMany`, and `afterDeleteMany`. | |
| **This guide is scoped to the lifecycle events currently emitted by the ObjectQL engine | |
| today**, which are listed below: | |
| | Event | When It Fires | Use Cases | | |
| |:------|:--------------|:----------| | |
| | **Read Operations** | | | | |
| | `beforeFind` | Before querying multiple records | Filter queries by user context, log access | | |
| | `afterFind` | After querying multiple records | Transform results, mask sensitive data | | |
| | **Write Operations** | | | | |
| | `beforeInsert` | Before creating a record | Set defaults, validate, normalize | | |
| | `afterInsert` | After creating a record | Trigger side effects, log creation | | |
| | `beforeUpdate` | Before updating a record | Validate changes, normalize values | | |
| | `afterUpdate` | After updating a record | Publish events, sync external systems | | |
| | `beforeDelete` | Before deleting a record | Enforce business rules, prevent unsafe deletes | | |
| | `afterDelete` | After deleting a record | Clean up related data, emit audit events | |
| | **Purpose** | Validation, enrichment, transformation | Side effects, notifications, logging | | ||
| | **Can modify** | `ctx.input` (mutable) | `ctx.result` (mutable) | | ||
| | **Can abort** | Yes (throw error → rollback) | No (operation already committed) | | ||
| | **Transaction** | Within transaction | After transaction (unless async: false) | | ||
| | **Error handling** | Aborts operation by default | Logged by default (configurable) | | ||
|
|
There was a problem hiding this comment.
The “Before vs After Hooks” table claims after* hooks “cannot abort” and that errors are “logged by default”. In the current ObjectQL implementation, hook handlers are awaited and exceptions propagate (triggerHooks does not catch), so an after* hook can still fail the operation (and may cause a rollback if running inside a transaction). Please adjust this guidance to reflect actual runtime behavior or document the exact error-handling semantics/guarantees.
| | **Purpose** | Validation, enrichment, transformation | Side effects, notifications, logging | | |
| | **Can modify** | `ctx.input` (mutable) | `ctx.result` (mutable) | | |
| | **Can abort** | Yes (throw error → rollback) | No (operation already committed) | | |
| | **Transaction** | Within transaction | After transaction (unless async: false) | | |
| | **Error handling** | Aborts operation by default | Logged by default (configurable) | | |
| | **Purpose** | Validation, enrichment, transformation | Post-operation enrichment, side effects, notifications, logging | | |
| | **Can modify** | `ctx.input` (mutable) | `ctx.result` (mutable) | | |
| | **Can abort / fail operation** | Yes — throwing aborts the operation | Yes — handlers are awaited, so throwing can still fail the overall operation | | |
| | **Transaction** | Typically runs before persistence within the active transaction | Depends on runtime execution scope; may still participate in rollback if executed inside the active transaction | | |
| | **Error handling** | Thrown errors propagate by default | Thrown errors also propagate by default unless explicitly caught by the runtime/caller | | |
| **Important:** In the current ObjectQL implementation, hook handlers are awaited and exceptions from `after*` hooks are not swallowed by `triggerHooks`. Treat `after*` hooks as part of the operation's observable failure path unless your runtime explicitly catches and handles those errors. |
| // Required: Handler function (inline or string reference) | ||
| handler: async (ctx: HookContext) => { | ||
| // Your logic here | ||
| }, | ||
|
|
There was a problem hiding this comment.
In the HookSchema example, handler is marked as required, but in the canonical HookSchema (@objectstack/spec/data), handler is optional. This mismatch can confuse implementers (e.g., hooks that are declared for later binding). Please align the documentation/sample with the actual schema requirements.
| #### `async` — Background Execution | ||
|
|
||
| Only applicable for `after*` events: | ||
|
|
||
| ```typescript | ||
| // Blocking (default) — runs within transaction | ||
| async: false | ||
|
|
||
| // Fire-and-forget — runs in background | ||
| async: true | ||
| ``` |
There was a problem hiding this comment.
This section describes async: true as “fire-and-forget” background execution and implies it won’t block transactions. The current ObjectQL engine awaits all hook handlers and does not implement background scheduling based on the HookSchema async flag. Please either remove/soften these guarantees or document the concrete runtime mechanism that executes async hooks (e.g., job queue integration) and where it is implemented.
| The structure of `ctx.input` varies by event: | ||
|
|
||
| **Insert operations:** | ||
| ```typescript | ||
| // beforeInsert, afterInsert | ||
| { | ||
| // All field values being inserted | ||
| name: 'Acme Corp', |
There was a problem hiding this comment.
The examples document ctx.input as if it contains record fields directly (e.g., ctx.input.name, ctx.input.status). In the current ObjectQL engine, hook context input is shaped as an envelope (e.g., insert: { data, options }, update: { id, data, options }, find: { ast, options }). Please update the examples (and the “structure varies by event” section) to match the actual runtime shape so hooks don’t silently read/write the wrong fields.
| ### Cross-Object API | ||
|
|
||
| Access other objects within the same transaction: | ||
|
|
||
| ```typescript | ||
| handler: async (ctx: HookContext) => { | ||
| // Get API for another object | ||
| const users = ctx.api?.object('user'); | ||
|
|
||
| // Query users |
There was a problem hiding this comment.
The guide presents ctx.api as the recommended cross-object CRUD API inside hooks. The current ObjectQL hook context construction does not populate api, so this will be undefined in practice. Please either document how to obtain a ScopedContext/ObjectRepository from ctx.ql (or how api is injected), or clearly note that ctx.api is not currently available by default.
| // In your plugin's onEnable() | ||
| export const onEnable = async (ctx: { ql: ObjectQL }) => { | ||
| ctx.ql.registerHook('beforeInsert', async (hookCtx) => { | ||
| // Handler logic | ||
| }, { | ||
| object: 'account', | ||
| priority: 100, | ||
| }); |
There was a problem hiding this comment.
In the programmatic registration example, the hook sets ctx.input.created_at, but the engine’s hook input for insert operations is enveloped (e.g., ctx.input.data). As written, this example won’t set the timestamp on the inserted record. Please adjust to write to the actual insert payload shape used by ObjectQL hooks.
| result: { | ||
| records: [{ id: '1', ... }, { id: '2', ... }], | ||
| total: 150, | ||
| } |
There was a problem hiding this comment.
The HookContext result examples show afterFind returning an object like { records, total }, but the current ObjectQL engine passes the raw array result into ctx.result for find operations. Please align these result shape examples with the engine’s behavior (or specify which API layer returns { records, total }).
| result: { | |
| records: [{ id: '1', ... }, { id: '2', ... }], | |
| total: 150, | |
| } | |
| result: [ | |
| { id: '1', ... }, | |
| { id: '2', ... }, | |
| ] |
|
|
||
| await ctx.api?.object('audit_log').insert({ | ||
| object_type: ctx.object, | ||
| record_id: String(ctx.input.id || ''), |
There was a problem hiding this comment.
In the audit trail example, record_id is derived from ctx.input.id, but for inserts the record id is only available on ctx.result after creation. As written, afterInsert audit records may store an empty record_id. Please use the appropriate source for each event (result.id for inserts; input.id for updates/deletes).
| await ctx.api?.object('audit_log').insert({ | |
| object_type: ctx.object, | |
| record_id: String(ctx.input.id || ''), | |
| const recordId = ctx.event === 'afterInsert' | |
| ? ctx.result?.id | |
| : ctx.input.id; | |
| await ctx.api?.object('audit_log').insert({ | |
| object_type: ctx.object, | |
| record_id: String(recordId || ''), |
| // Optional: Conditional execution | ||
| condition: "status = 'active' AND amount > 1000", | ||
|
|
||
| // Optional: Human-readable description | ||
| description: 'Validates account data before save', | ||
|
|
||
| // Optional: Error handling strategy | ||
| onError: 'abort', // 'abort' | 'log' | ||
|
|
||
| // Optional: Execution timeout (ms) | ||
| timeout: 5000, | ||
|
|
||
| // Optional: Retry policy |
There was a problem hiding this comment.
The guide presents onError / timeout / retryPolicy / condition as runtime behavior controls, but the current ObjectQL hook execution path doesn’t apply these (no condition evaluation, timeout enforcement, retry logic, or per-hook error policy in triggerHooks). Please either document them as schema-only fields that need additional runtime support, or link to the implementation that applies them so developers know when they take effect.
| // Optional: Conditional execution | |
| condition: "status = 'active' AND amount > 1000", | |
| // Optional: Human-readable description | |
| description: 'Validates account data before save', | |
| // Optional: Error handling strategy | |
| onError: 'abort', // 'abort' | 'log' | |
| // Optional: Execution timeout (ms) | |
| timeout: 5000, | |
| // Optional: Retry policy | |
| // Optional schema field: conditional execution metadata. | |
| // Note: The default ObjectQL hook execution path does not currently evaluate | |
| // `condition` automatically; additional runtime support is required. | |
| condition: "status = 'active' AND amount > 1000", | |
| // Optional: Human-readable description | |
| description: 'Validates account data before save', | |
| // Optional schema field: error handling metadata. | |
| // Note: Per-hook `onError` behavior is not currently enforced automatically | |
| // by the default ObjectQL `triggerHooks` execution path. | |
| onError: 'abort', // 'abort' | 'log' | |
| // Optional schema field: timeout metadata. | |
| // Note: Timeouts are not currently enforced automatically by the default | |
| // ObjectQL hook execution path. | |
| timeout: 5000, | |
| // Optional schema field: retry metadata. | |
| // Note: Retry behavior is not currently applied automatically by the default | |
| // ObjectQL hook execution path. |
Created comprehensive documentation for third-party developers writing data lifecycle hooks in ObjectStack.
Files Added
skills/objectstack-hooks/SKILL.md(1,038 lines) — Complete guide covering:beforeInsert,afterUpdate, etc.)name,object,events,handler,priority,async,condition, etc.)HookContextAPI reference (input,result,previous,session,api, etc.)skills/objectstack-hooks/references/— Schema references and documentation indexExample Usage
Structure
Follows existing skill conventions (objectstack-data, objectstack-kernel, etc.) with:
@objectstack/specComplements existing skills: objectstack-data (objects/fields), objectstack-kernel (plugins/services), objectstack-automation (flows/workflows).