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
24 changes: 24 additions & 0 deletions .changeset/18163-export-hook-api-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@objectstack/spec': minor
---

`@objectstack/spec/data` now exports the typed hook `ctx.api` face — `HookApi`, `HookObjectApi`, `HookQuery`, `HookCountQuery`, `HookUpdateDoc`, `HookUpdateOptions`, `HookDeleteOptions`, `HookDoc` and `HookDriverPassthroughOptions` — so a metadata app's `*.hook.ts` imports the platform's type instead of hand-declaring one (#18163). The same entry additionally re-exports `EngineTransactionInfo` and `EngineTransactionOptions`, which its public declarations reference structurally: without them a consumer that imports only `@objectstack/spec/data` and emits declarations answers `TS2883: The inferred type ... cannot be named without a reference to ...`. Type-only re-exports of the declarations `@objectstack/spec/contracts` already publishes, not second declarations.

```ts
import type { HookApi } from '@objectstack/spec/data';

const api = ctx.api as HookApi | undefined;
if (!api) return;
const owner = await api.object('user').findOne({ where: { id: ctx.input.owner } });
```

The platform already implemented this surface; it just never published a type an app could import, so every app re-derived the engine's option vocabulary in a copy that drifts the moment the engine moves. The reference third-party app carried ~2,358 authored tokens of one in a single file, imported by 17 hook files.

- **The query shape is `where`-only — there is no `filter` key, deliberately.** `RPC_QUERY_ALIAS_SLOTS` declares `filter` as the alias of `where` (and `top` as the alias of `limit`); every engine entry point folds the `where` slot, collapsing redundant identical spellings and REFUSING the slot when the two spellings carry different values. So `{ where, filter }` is silent when they happen to agree and a runtime throw when they do not. Omitting the alias keys makes it neither: `TS2353: 'filter' does not exist in type 'HookQuery'`, at the authoring site.
- **Not a second dialect of `IScopedContext`.** `contracts/scoped-context.ts` stays the CHECKED IMPLEMENTATION contract ObjectQL's `ScopedContext` and `ObjectRepository` carry `implements` clauses against, with its deliberately loose `Record<string, unknown>` bags. This is the authoring half of the same seam: `HookApi` is assignable to `IScopedContext`, so `ctx.api as HookApi` stays a direct cast, and nothing about the older contract changes.
- **Every option shape is DERIVED, not transcribed.** Each is an `Omit`/`Pick` over the `Engine*Options` schemas that the engine's own per-method legal-key sets are pinned against, so a key added to a schema reaches the published type in the same run it reaches the engine's accepted set. `count` is the one shape without the driver pass-through keys, because the engine forwards no bag on that method and rejects them there — engine behaviour no document states, and exactly what a hand-written copy gets wrong.
- **What is deliberately absent, each for a stated reason**: `context` (the repository injects it and discards a caller's), the `cursor` / `distinct` / `upsert` tombstones, `sudo()` (the #5945 exclusion stands — `Hook.runAs: 'system'` is the declared way to run elevated), and `aggregate` / `execute` / `create` / `deleteById`.

Additive only: eleven new exported names from `./data` (nine new declarations plus two type-only re-exports), no removal and no signature change, so nothing an existing consumer imports moves.

Clause-②: yes (widening)
124 changes: 122 additions & 2 deletions packages/spec/api-surface-declarations/data.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
# excluded: documentation drift is `check:docs`'s axis, not this one.
#
# entry: ./data
# exported names: 833
# declarations: 846
# exported names: 844
# declarations: 857
#
# GENERATED — ⛔ never hand-edited. Regenerate after a real build:
# pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec gen:api-surface-declarations
Expand Down Expand Up @@ -6167,6 +6167,46 @@ declare const EngineQueryOptionsSchema: z.ZodObject<{
distinct: z.ZodOptional<z.ZodNever>;
}, z.core.$strip>;

// ── EngineTransactionInfo (interface) ──
interface EngineTransactionInfo {
/**
* `true` when THIS call opened the transaction and therefore owns its
* commit/rollback; `false` when it JOINED an already-open ambient one
* (ADR-0067 D2) and some outer caller owns the outcome.
*
* The join is correct and stays — a nested `begin` would take a second
* connection (deadlocking a single-connection SQLite pool) and would not be
* covered by the outer rollback. What was missing is that the callback
* could not TELL: a joined callback's `throw` unwinds work the outer owner
* may still commit or roll back on its own terms, and guarantees phrased
* as "this whole unit rolls back together" (`batchData`'s rollback
* response, ADR-0119 D4) hold only for the owner. A callback that must not
* promise what it does not control reads `owned` and says so.
*/
owned: boolean;
}

// ── EngineTransactionOptions (interface) ──
interface EngineTransactionOptions {
/**
* Fail CLOSED when the datasource cannot give a real transaction.
*
* Default (`undefined` / `false`) keeps ADR-0119 D1's declared degrade: a
* driver without `beginTransaction` runs the callback with no transaction
* and no rollback, warning once. That degrade is right for callers who can
* live without atomicity (test doubles, in-memory drivers) and wrong for
* callers whose whole reason to open a transaction is the rollback.
*
* With `require: true` the engine THROWS instead of degrading, before the
* callback runs — so a caller that cannot tolerate losing atomicity states
* it once, at the call site, instead of re-deriving `batchData`'s probe.
* That probe is the precedent being generalized here (ADR-0119 D4, cited in
* older text as ADR-0118 D4 — see that ADR's renumbering note): an `atomic`
* request refuses rather than silently running best-effort.
*/
require?: boolean;
}

// ── EngineUpdateOptions (type) ──
type EngineUpdateOptions = z.input<typeof EngineUpdateOptionsSchema>;

Expand Down Expand Up @@ -14276,6 +14316,26 @@ declare const GroupByNodeSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
// ── Hook (type) ──
type Hook = z.input<typeof HookSchema>;

// ── HookApi (interface) ──
interface HookApi {
/** The repository for `name`, bound to this context. */
object(name: string): HookObjectApi;
/**
* Run `callback` inside one driver transaction: committed when it returns,
* rolled back when it throws.
*
* The callback receives a NEW `HookApi` whose operations share the
* transaction handle — reach objects through THAT context (`tx.object(…)`),
* not the outer one, or the writes land outside the transaction.
*
* The second parameter is declared because the PRODUCER hands it
* unconditionally (`ScopedContext.transaction`, #5696). Contravariance keeps
* the zero- and one-argument callbacks authors actually write assignable, so
* the truthful signature is also the more permissive one.
*/
transaction<T>(callback: (trxCtx: HookApi, info: EngineTransactionInfo) => Promise<T>, opts?: EngineTransactionOptions): Promise<T>;
}

// ── HookBody (type) ──
type HookBody = z.input<typeof HookBodySchema>;

Expand Down Expand Up @@ -14370,9 +14430,21 @@ declare const HookContextSchema: z.ZodObject<{
}, z.core.$strip>>;
}, z.core.$strip>;

// ── HookCountQuery (type) ──
type HookCountQuery = Omit<EngineCountOptions, 'context'>;

// ── HookDeleteOptions (type) ──
type HookDeleteOptions = Omit<EngineDeleteOptions, 'context'> & HookDriverPassthroughOptions;

// ── HookDispatch (type) ──
type HookDispatch = NonNullable<HookContext['dispatch']>;

// ── HookDoc (type) ──
type HookDoc = Record<string, unknown>;

// ── HookDriverPassthroughOptions (type) ──
type HookDriverPassthroughOptions = Pick<DriverOptions, 'transaction' | 'tenantId' | 'tenantIds' | 'timezone' | 'bypassTenantAudit' | 'preserveAudit'>;

// ── HookEvent (const) ──
declare const HookEvent: z.ZodEnum<{
beforeInsert: "beforeInsert";
Expand All @@ -14388,9 +14460,51 @@ declare const HookEvent: z.ZodEnum<{
// ── HookEventType (type) ──
type HookEventType = z.input<typeof HookEvent>;

// ── HookObjectApi (interface) ──
interface HookObjectApi {
/**
* Read every record the query selects.
*
* `Promise<any[]>` mirrors `IScopedObjectRepository.find` and
* `IDataEngine.find`; narrowing it here would make this face disagree with
* the two declarations it forwards through.
*/
find(query?: HookQuery): Promise<any[]>;
/** Read the ONE record the query selects, or `null`. */
findOne(query?: HookQuery): Promise<Record<string, any> | null>;
/** Count the records the query selects. */
count(query?: HookCountQuery): Promise<number>;
/** Insert one record, or an array of records. */
insert(data: HookDoc | HookDoc[]): Promise<any>;
/**
* Update records: the record for the single-record form, the affected-row
* count for the predicate form (`{ where, multi: true }`), `null` when the
* write matched nothing.
*/
update(data: HookUpdateDoc, options?: HookUpdateOptions): Promise<Record<string, any> | number | null>;
/**
* Update a single record by id — the id travels as the first argument.
*
* Answers the written record, or `null` when the id matched nothing. A falsy
* id is not a narrower answer but a REFUSAL: `0` and `''` identify no row, so
* the dispatch rejects and the call throws.
*/
updateById(id: string | number, data: HookUpdateDoc): Promise<Record<string, any> | null>;
/**
* Delete records — `{ where, multi: true }` for the predicate form.
*
* `Promise<boolean | number>` is what `IDataEngine.delete` declares, one door
* down from this method.
*/
delete(options?: HookDeleteOptions): Promise<boolean | number>;
}

// ── HookParsed (type) ──
type HookParsed = z.infer<typeof HookSchema>;

// ── HookQuery (type) ──
type HookQuery = Omit<EngineQueryOptions, 'context' | 'top' | 'cursor' | 'distinct'> & HookDriverPassthroughOptions;

// ── HookSchema (const) ──
declare const HookSchema: z.ZodObject<{
_lock: z.ZodOptional<z.ZodEnum<{
Expand Down Expand Up @@ -14484,6 +14598,12 @@ declare const HookSchema: z.ZodObject<{
}>>;
}, z.core.$strict>;

// ── HookUpdateDoc (type) ──
type HookUpdateDoc = HookDoc;

// ── HookUpdateOptions (type) ──
type HookUpdateOptions = Omit<EngineUpdateOptions, 'context' | 'upsert'> & WriteObservabilityOptions & HookDriverPassthroughOptions;

// ── IMPORT_BOOLEAN_FALSE_TOKENS (const) ──
declare const IMPORT_BOOLEAN_FALSE_TOKENS: ReadonlySet<string>;

Expand Down
11 changes: 11 additions & 0 deletions packages/spec/api-surface/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@
"EngineQueryOptions (type)",
"EngineQueryOptionsParsed (type)",
"EngineQueryOptionsSchema (const)",
"EngineTransactionInfo (interface)",
"EngineTransactionOptions (interface)",
"EngineUpdateOptions (type)",
"EngineUpdateOptionsSchema (const)",
"EqualityOperatorSchema (const)",
Expand Down Expand Up @@ -337,18 +339,27 @@
"GroupByNode (type)",
"GroupByNodeSchema (const)",
"Hook (type)",
"HookApi (interface)",
"HookBody (type)",
"HookBodyCapability (const)",
"HookBodyCapability (type)",
"HookBodyParsed (type)",
"HookBodySchema (const)",
"HookContext (type)",
"HookContextSchema (const)",
"HookCountQuery (type)",
"HookDeleteOptions (type)",
"HookDispatch (type)",
"HookDoc (type)",
"HookDriverPassthroughOptions (type)",
"HookEvent (const)",
"HookEventType (type)",
"HookObjectApi (interface)",
"HookParsed (type)",
"HookQuery (type)",
"HookSchema (const)",
"HookUpdateDoc (type)",
"HookUpdateOptions (type)",
"IMPORT_BOOLEAN_FALSE_TOKENS (const)",
"IMPORT_BOOLEAN_TRUE_TOKENS (const)",
"IMPORT_REFERENCE_TYPES (const)",
Expand Down
11 changes: 11 additions & 0 deletions packages/spec/export-origins/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@
"EngineQueryOptions": "src/data/data-engine.zod.ts#EngineQueryOptions (type)",
"EngineQueryOptionsParsed": "src/data/data-engine.zod.ts#EngineQueryOptionsParsed (type)",
"EngineQueryOptionsSchema": "src/data/data-engine.zod.ts#EngineQueryOptionsSchema (const)",
"EngineTransactionInfo": "src/contracts/objectql-engine.ts#EngineTransactionInfo (interface)",
"EngineTransactionOptions": "src/contracts/objectql-engine.ts#EngineTransactionOptions (interface)",
"EngineUpdateOptions": "src/data/data-engine.zod.ts#EngineUpdateOptions (type)",
"EngineUpdateOptionsSchema": "src/data/data-engine.zod.ts#EngineUpdateOptionsSchema (const)",
"EqualityOperatorSchema": "src/data/filter.zod.ts#EqualityOperatorSchema (const)",
Expand Down Expand Up @@ -328,17 +330,26 @@
"GroupByNode": "src/data/query.zod.ts#GroupByNode (type)",
"GroupByNodeSchema": "src/data/query.zod.ts#GroupByNodeSchema (const)",
"Hook": "src/data/hook.zod.ts#Hook (type)",
"HookApi": "src/data/hook-api.ts#HookApi (interface)",
"HookBody": "src/data/hook-body.zod.ts#HookBody (type)",
"HookBodyCapability": "src/data/hook-body.zod.ts#HookBodyCapability (type)",
"HookBodyParsed": "src/data/hook-body.zod.ts#HookBodyParsed (type)",
"HookBodySchema": "src/data/hook-body.zod.ts#HookBodySchema (const)",
"HookContext": "src/data/hook.zod.ts#HookContext (type)",
"HookContextSchema": "src/data/hook.zod.ts#HookContextSchema (const)",
"HookCountQuery": "src/data/hook-api.ts#HookCountQuery (type)",
"HookDeleteOptions": "src/data/hook-api.ts#HookDeleteOptions (type)",
"HookDispatch": "src/data/hook.zod.ts#HookDispatch (type)",
"HookDoc": "src/data/hook-api.ts#HookDoc (type)",
"HookDriverPassthroughOptions": "src/data/hook-api.ts#HookDriverPassthroughOptions (type)",
"HookEvent": "src/data/hook.zod.ts#HookEvent (const)",
"HookEventType": "src/data/hook.zod.ts#HookEventType (type)",
"HookObjectApi": "src/data/hook-api.ts#HookObjectApi (interface)",
"HookParsed": "src/data/hook.zod.ts#HookParsed (type)",
"HookQuery": "src/data/hook-api.ts#HookQuery (type)",
"HookSchema": "src/data/hook.zod.ts#HookSchema (const)",
"HookUpdateDoc": "src/data/hook-api.ts#HookUpdateDoc (type)",
"HookUpdateOptions": "src/data/hook-api.ts#HookUpdateOptions (type)",
"IMPORT_BOOLEAN_FALSE_TOKENS": "src/data/import-coercion.ts#IMPORT_BOOLEAN_FALSE_TOKENS (const)",
"IMPORT_BOOLEAN_TRUE_TOKENS": "src/data/import-coercion.ts#IMPORT_BOOLEAN_TRUE_TOKENS (const)",
"IMPORT_REFERENCE_TYPES": "src/data/import-coercion.ts#IMPORT_REFERENCE_TYPES (const)",
Expand Down
Loading
Loading