Skip to content

Commit ea2940d

Browse files
os-billclaude
andauthored
fix(spec): ActionEngineFacade.delete declares the id array the runtime already accepts, and says which convention is the contract (#17608)
* fix(spec): declare ActionEngineFacade.delete's id array, and say which convention is the contract `ActionEngineFacade.delete` declared `id: string` while the runtime facade has always accepted `string | string[]`, iterating one `ql.delete` per id. The declaration is now `string | string[]`, and the member's doc comment states what the runtime comment used to record as an accident of two handler suites: both spellings are contract, the array form is a convenience over the same per-row path, and it is neither bulk nor atomic. Two consumers follow from the declaration: - `packages/runtime`'s "tolerant of both conventions" comment is retired (a comment correction only; the arm's behaviour is untouched). - `examples/app-todo/src/actions/task.handlers.ts` drops the hand-rolled `ActionContext` copy of the facade — which existed because the published type could not express its array call, and which had already drifted on `find` — for the published `ActionHandlerContext`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * chore(changeset): patch for the ActionEngineFacade.delete declaration (#15117) The level is measured, not assumed: nothing is removed and nothing narrows, no runtime behaviour changes, and the sibling re-declaration of the neighbouring member (`find`, #14175) shipped as a patch from the same interface. What ships is a published type that finally describes behaviour that was already served. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * docs(spec): cite the runtime delete arm by symbol, not by line number (#15117) `check:spec-docblock-symbol-anchors` refuses a line number as an anchor form, and the citation added with the widening (`:1471`) was one — a NEW finding, not one of the seven day-one residuals. It is now the symbol anchor `packages/runtime/src/action-execution.ts#buildActionEngineFacade`, which is also the more honest citation: this card exists partly because the line numbers the issue quoted had already drifted by three hundred lines. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * fix(spec): grade the delete widening as minor, and re-verify the flagged docs page (#15117) Contract review returned FAIL with two must-fixes. 1. The changeset is `minor`, not `patch`. The written rule (`.github/workflows/pr-automation.yml`, maintainer ruling 2026-09-04 batch #35) is that a purely additive widening of a published package's public surface takes at least `minor`, and a commit type may raise a bump but never lower it below what the act requires. The PR's own `Clause-②: yes` line says this widens the accept set, in those words. The `find` precedent it leaned on does not reach: that was a NARROWING, it landed the day the rule was ruled, and the rule disclaims pre-rule `patch` precedents. How the wrong level survived local verification is the more useful half: `check-changeset-no-major` reads the clause-② declaration from the event payload and nothing else, so a local run without `--event` cannot exercise the level axis at all. Its exit 0 was recorded as a reading when the instrument could not have come back the other way. 2. `content/docs/ui/actions.mdx` — the page the repo's own Docs Drift Check flagged on this PR — is re-verified against the rewritten example. Its handler snippet still annotated `ctx: ActionContext`, a type that file no longer declares; it now imports and annotates the published `ActionHandlerContext`. The same snippet also wrote `completed_date`, which is `readonly` on `todo_task` and stamped by the object's `beforeUpdate` hook: copying it made the action refuse itself against `completed_date_required`. Both facts are verified against `task.object.ts` and `task.hook.ts`. The page now states the `delete` convention beside where it already states `find`'s. Folded in: the example's comment attributed to the contract a request the contract does not make. The contract asks for `ActionHandler`; it says so, and says why a file of function declarations annotates `ActionHandlerContext` instead. Comment-only — 11 changed lines, all comments or blank. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH * docs(ui): state the true reason a handler must not name a server-owned field (#15117) The previous push removed `completed_date` from the handler snippet, which was right, and then explained it with a mechanism that is false. Re-measured: - A handler's `ctx.engine` runs ELEVATED — `buildActionExecutionContext` returns `{ ...base, isSystem: true }` (`packages/runtime/src/action-execution.ts`) — and the read-only strip is gated `if (!opCtx.context?.isSystem)` (`packages/objectql/src/engine.ts`). Nothing is stripped on that path, so "stripped from the payload before the record is validated" never happens. - The refusal does not exist either, for any caller. The hook's stamp is unconditional on the transition and its own docblock says that is precisely so a caller-supplied value is overwritten and survives the strip; a live test asserts it — "a caller that still sends `completed_date` is not punished for it — the hook value wins". The REJECTED row whose mechanism the page described is labelled "Measured before the fix". The true reason is close to the inverse, and is now what the page says: the write is not stripped, it LANDS, and on a write that is not a completion transition the hook does not stamp, so the handler's "now" silently replaces the real completion timestamp. I read the #7036 history at the three sites I cited and wrote it in the present tense, without reading the four sites the conclusion depended on. A citation that exists is not a citation that entails. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c1123cf commit ea2940d

6 files changed

Lines changed: 205 additions & 34 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): `ActionEngineFacade.delete` declares the id ARRAY the runtime has always accepted, and says which convention is the contract (#15117)
6+
7+
`delete(object, id: string)` declared one id. The runtime facade
8+
(`buildActionEngineFacade` in `packages/runtime`) has accepted `string | string[]`
9+
all along — normalising the argument and issuing one `ql.delete` per id — and
10+
described that in a comment as a tolerance two handler suites happened to cause.
11+
The declaration was simply behind the behaviour, and the one first-party suite on
12+
the array form could only reach it by hand-rolling a private copy of the
13+
interface (a copy that had already drifted on `find`).
14+
15+
The slot is now `delete(object: string, idOrIds: string | string[])`, and the
16+
member's doc comment states the contract instead of leaving it to be inferred
17+
from a runtime comment two packages away:
18+
19+
- **Both spellings are contract.** One row is `delete(object, id)`; a set is
20+
`delete(object, ids)` — a handler holding a list does not have to unroll it
21+
into a loop to stay on the contract.
22+
- **The array form is a convenience over the same per-row path** — not a bulk or
23+
atomic delete. There is no transaction around the set: a failure part-way
24+
leaves the ids before it deleted. An empty array deletes nothing and resolves.
25+
26+
Nothing is removed and nothing narrows: every existing single-id call still
27+
type-checks, and no runtime behaviour changes — this release makes the published
28+
type describe what was already being served. That makes it non-breaking, not a
29+
patch: widening a published parameter is a purely additive widening of a public
30+
surface, which takes at least `minor` whatever the commit type says. Handler authors who copied the
31+
facade into a local context type to reach the array form can delete the copy and
32+
annotate with `ActionHandlerContext` / `ActionHandler` from `@objectstack/spec/ui`.

content/docs/ui/actions.mdx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,39 @@ For logic that belongs in real source files, point `target` at a handler name
137137
and register it in your config's `onEnable` lifecycle hook:
138138

139139
```typescript title="src/actions/task.handlers.ts"
140-
export async function completeTask(ctx: ActionContext): Promise<void> {
141-
const { record, engine } = ctx; // ctx = { record, user, engine, params }
140+
import type { ActionHandlerContext } from '@objectstack/spec/ui';
141+
142+
export async function completeTask(ctx: ActionHandlerContext): Promise<void> {
143+
const { record, engine } = ctx; // ctx = { record, params, user, session, engine }
142144
await engine.update('todo_task', record.id as string, {
143145
status: 'completed',
144-
completed_date: new Date().toISOString(),
145146
});
146147
}
147148
```
148149

150+
Annotate `ctx` with the **published** `ActionHandlerContext`, never a local copy
151+
of it: a hand-rolled context interface drifts from the contract the moment either
152+
side moves, and the drift is silent because the copy is what your handler is
153+
checked against. (The contract's own docblock asks for `ActionHandler`; that is a
154+
function type, so it fits an arrow-function handler assigned to a `const`, while
155+
a function *declaration* like the one above annotates its parameter instead.)
156+
157+
Write only the fields the action owns — and read that literally, because a
158+
handler's `ctx.engine` runs **elevated** (`isSystem`). The read-only strip that
159+
protects a server-owned column from an ordinary caller is gated on *not* being
160+
a system write, so it does not run on a handler's write at all. **What a
161+
handler names, lands.** A server-owned field is therefore more dangerous here
162+
than in a form PUT, not less.
163+
164+
`completed_date` is the example. It is `readonly` on `todo_task` — the server
165+
owns it, and the object's `beforeUpdate` hook stamps it on the completion
166+
**transition**. Adding `completed_date: new Date().toISOString()` to the write
167+
above looks harmless, and on that transition it is: the hook overwrites it. But
168+
a completion write that is *not* a transition — re-completing a task that is
169+
already `completed` — is not stamped, so nothing overwrites the handler's value
170+
and nothing strips it either, and the action silently replaces the real
171+
completion timestamp with "now". That is why the snippet sends `status` alone.
172+
149173
<Callout type="warn">
150174
**`ctx.engine.find(object, filter)` takes a filter, not a query.** The second
151175
argument is the `where` half only — `{ status: 'completed' }`, operators
@@ -161,6 +185,24 @@ refuses a primitive or a mistyped `$and` / `$or` / `$not` but still admits
161185
double must honour it too.
162186
</Callout>
163187

188+
<Callout type="info">
189+
**`ctx.engine.delete(object, id)` takes one id — or an array of them.** Both
190+
spellings are contract: `ActionEngineFacade` in `@objectstack/spec/ui` declares
191+
the parameter `string | string[]`, so a handler that already holds a list does
192+
not have to unroll it into a loop to stay on the contract.
193+
194+
```typescript
195+
await ctx.engine.delete('todo_task', record.id as string); // one row
196+
await ctx.engine.delete('todo_task', ['tsk_0001', 'tsk_0002']); // a set
197+
```
198+
199+
The array form is a **convenience over the same per-row path** — not a bulk and
200+
not an atomic delete. The runtime issues one delete per id, in order, with no
201+
transaction around the set: a failure part-way through leaves the ids before it
202+
deleted and the ids after it untouched, and the rejection you see is the one that
203+
stopped it. An empty array deletes nothing and resolves.
204+
</Callout>
205+
164206
```typescript title="objectstack.config.ts"
165207
export const onEnable = async (ctx: { ql: { registerAction: (...args: unknown[]) => void } }) => {
166208
ctx.ql.registerAction('todo_task', 'completeTask', completeTask);

examples/app-todo/src/actions/task.handlers.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,30 @@
1414
* ```
1515
*/
1616

17-
// ─── Handler Context (simplified for example purposes) ──────────────
18-
interface ActionContext {
19-
/** The record being acted upon */
20-
record: Record<string, unknown>;
21-
/** Current authenticated user */
22-
user: { id: string; name: string };
23-
/** Data engine for CRUD operations */
24-
engine: {
25-
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
26-
insert(object: string, data: Record<string, unknown>): Promise<{ id: string }>;
27-
find(object: string, query: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
28-
delete(object: string, ids: string[]): Promise<void>;
29-
};
30-
/** Action parameters (from user input / params) */
31-
params?: Record<string, unknown>;
32-
}
17+
import type { ActionHandlerContext } from '@objectstack/spec/ui';
18+
19+
// ─── Handler Context ─────────────────────────────────────
20+
//
21+
// `ActionHandlerContext` (`@objectstack/spec/ui`) is the PUBLISHED contract for
22+
// what an action body reads as `ctx` — `record`, `params`, `user`, `session` and
23+
// the trusted `engine` facade.
24+
//
25+
// What the contract asks for in its own words is `ActionHandler`. That is a
26+
// FUNCTION type, and the handlers below are function DECLARATIONS, which cannot
27+
// carry one; rebinding them as `const cloneTask: ActionHandler = ...` would also
28+
// erase their return types, because `ActionHandler` returns `unknown`. Annotating
29+
// the ctx parameter with `ActionHandlerContext` — the type `ActionHandler` is
30+
// defined in terms of — is the same contract, reached the way this file is
31+
// written.
32+
//
33+
// This file used to declare a local simplified copy of that context instead,
34+
// and the copy drifted: its `find` still took an ObjectQL-shaped `query` bag
35+
// long after the contract had settled on a FILTER (#14175). It existed because
36+
// `ActionEngineFacade.delete` was declared as a single id while the runtime had
37+
// always accepted an id array too, so `deleteCompletedTasks` below could not be
38+
// written against the published type at all. The declaration now says
39+
// `string | string[]` (#15117), so the copy is gone and this example
40+
// type-checks against exactly the types a real app gets.
3341

3442
/**
3543
* Mark a single task as complete.
@@ -41,23 +49,23 @@ interface ActionContext {
4149
* `beforeUpdate` leg of `src/objects/task.hook.ts`, which runs on the
4250
* transition and whose write the strip lets through.
4351
*/
44-
export async function completeTask(ctx: ActionContext): Promise<void> {
52+
export async function completeTask(ctx: ActionHandlerContext): Promise<void> {
4553
const { record, engine } = ctx;
4654
await engine.update('todo_task', record.id as string, {
4755
status: 'completed',
4856
});
4957
}
5058

5159
/** Mark a task as in-progress */
52-
export async function startTask(ctx: ActionContext): Promise<void> {
60+
export async function startTask(ctx: ActionHandlerContext): Promise<void> {
5361
const { record, engine } = ctx;
5462
await engine.update('todo_task', record.id as string, {
5563
status: 'in_progress',
5664
});
5765
}
5866

5967
/** Clone a task (duplicate with reset status) */
60-
export async function cloneTask(ctx: ActionContext): Promise<{ id: string }> {
68+
export async function cloneTask(ctx: ActionHandlerContext): Promise<{ id: string }> {
6169
const { record, engine } = ctx;
6270
const { id, created_at, updated_at, completed_date, ...fields } = record as Record<string, unknown>;
6371
return engine.insert('todo_task', {
@@ -68,7 +76,7 @@ export async function cloneTask(ctx: ActionContext): Promise<{ id: string }> {
6876
}
6977

7078
/** Mark all selected tasks as complete (bulk) — same `status`-only rule as {@link completeTask} (#7036) */
71-
export async function massCompleteTasks(ctx: ActionContext): Promise<void> {
79+
export async function massCompleteTasks(ctx: ActionHandlerContext): Promise<void> {
7280
const { params, engine } = ctx;
7381
const ids = (params?.selectedIds ?? []) as string[];
7482
for (const id of ids) {
@@ -79,7 +87,7 @@ export async function massCompleteTasks(ctx: ActionContext): Promise<void> {
7987
}
8088

8189
/** Delete all completed tasks */
82-
export async function deleteCompletedTasks(ctx: ActionContext): Promise<void> {
90+
export async function deleteCompletedTasks(ctx: ActionHandlerContext): Promise<void> {
8391
const { engine } = ctx;
8492
const completed = await engine.find('todo_task', { status: 'completed' });
8593
const ids = completed.map((r) => r.id as string);
@@ -89,7 +97,7 @@ export async function deleteCompletedTasks(ctx: ActionContext): Promise<void> {
8997
}
9098

9199
/** Defer a task by updating its due date (params collected by the action dialog) */
92-
export async function deferTask(ctx: ActionContext): Promise<void> {
100+
export async function deferTask(ctx: ActionHandlerContext): Promise<void> {
93101
const { record, engine, params } = ctx;
94102
await engine.update('todo_task', record.id as string, {
95103
due_date: params?.new_due_date ? String(params.new_due_date) : null,
@@ -99,7 +107,7 @@ export async function deferTask(ctx: ActionContext): Promise<void> {
99107
}
100108

101109
/** Set a reminder on a task (params collected by the action dialog) */
102-
export async function setReminder(ctx: ActionContext): Promise<void> {
110+
export async function setReminder(ctx: ActionHandlerContext): Promise<void> {
103111
const { record, engine, params } = ctx;
104112
await engine.update('todo_task', record.id as string, {
105113
reminder_date: params?.reminder_date ? String(params.reminder_date) : null,
@@ -108,7 +116,7 @@ export async function setReminder(ctx: ActionContext): Promise<void> {
108116
}
109117

110118
/** Export tasks to CSV format */
111-
export async function exportTasksToCSV(ctx: ActionContext): Promise<string> {
119+
export async function exportTasksToCSV(ctx: ActionHandlerContext): Promise<string> {
112120
const { engine } = ctx;
113121
const tasks = await engine.find('todo_task', {});
114122
const header = 'subject,status,priority,category,due_date';

packages/runtime/src/action-execution.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,8 +1466,8 @@ export function buildActionEngineFacade(_deps: ActionExecutionDeps, ql: any, ec?
14661466
async update(object: string, id: string, data: Record<string, unknown>): Promise<void> {
14671467
await ql.update(object, data, { where: { id }, context });
14681468
},
1469-
// Tolerant of both the single-id and array conventions handler suites
1470-
// use (CRM handlers pass one id; todo handlers pass an id array).
1469+
// Both spellings are DECLARED contract (#15117), not a tolerance: the
1470+
// spec's `ActionEngineFacade.delete` takes `string | string[]`.
14711471
async delete(object: string, idOrIds: string | string[]): Promise<void> {
14721472
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
14731473
for (const id of ids) {

packages/spec/src/ui/action-params.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,58 @@ describe('#14175 — ActionEngineFacade.find takes a FILTER (the `where` half),
468468
expect('where' in envelope && 'where' in nested).toBe(true);
469469
});
470470
});
471+
472+
// ---------------------------------------------------------------------------
473+
// #15117 — `ActionEngineFacade.delete` takes ONE id, or an ARRAY of ids
474+
// ---------------------------------------------------------------------------
475+
476+
// The declared slot, read off the interface — not a retyped copy of it, so a
477+
// re-narrowing back to the single-id `string` this card retired fails HERE,
478+
// rather than in the first handler that hands the facade a list.
479+
type DeleteIds = Parameters<ActionEngineFacade['delete']>[1];
480+
481+
// The type-level pin (the tsc channel, `tsc -p tsconfig.test.json`). `Eq` is
482+
// the strict mutual-assignability test, so the pre-#15117 declaration
483+
// (`string` alone) does not satisfy it — neither does a slot widened all the
484+
// way to `unknown`. Exported for the same reason the sibling pins are.
485+
export type DeleteIdsAcceptsOneOrMany = Assert< Eq< DeleteIds, string | string[] > >;
486+
487+
describe('#15117 — ActionEngineFacade.delete accepts one id or an array, both as contract', () => {
488+
it('types the second parameter as `string | string[]` (the tsc channel)', () => {
489+
// The value-level half of `DeleteIdsAcceptsOneOrMany` above: literals
490+
// annotated with the slot type, so the runtime run exercises the same
491+
// declaration the type pin reads.
492+
const one: DeleteIds = 'tsk_0001';
493+
const many: DeleteIds = ['tsk_0001', 'tsk_0002'];
494+
495+
expect([typeof one, Array.isArray(many)]).toEqual(['string', true]);
496+
});
497+
498+
it('positive control — both handler conventions compile, and so does the empty set', () => {
499+
// The single-id convention (the CRM handler suites).
500+
const singleId: DeleteIds = 'tsk_0001';
501+
// The array convention (`examples/app-todo`'s `deleteCompletedTasks`),
502+
// which before this card was reachable only through a hand-rolled copy of
503+
// `ActionEngineFacade` — the workaround the widening retired.
504+
const idArray: DeleteIds = ['tsk_0001', 'tsk_0002', 'tsk_0003'];
505+
// An empty list deletes nothing and resolves; the member doc says so.
506+
const empty: DeleteIds = [];
507+
508+
expect([singleId, idArray, empty].length).toBe(3);
509+
});
510+
511+
it('refuses at compile time what neither convention admits', () => {
512+
// Each `@ts-expect-error` is itself checked: if the slot ever ADMITS one
513+
// of these, the directive goes unused and `tsc -p tsconfig.test.json` reds.
514+
// @ts-expect-error — an id is a string; a number is not an id under either convention.
515+
const numericId: DeleteIds = 42;
516+
// @ts-expect-error — the array form is an array of ids, not of numbers.
517+
const numericIds: DeleteIds = [1, 2];
518+
// @ts-expect-error — one level of array, never a nested one.
519+
const nestedIds: DeleteIds = [['tsk_0001']];
520+
// @ts-expect-error — "delete nothing" is the EMPTY ARRAY, never a null id.
521+
const nullId: DeleteIds = null;
522+
523+
expect([numericId, numericIds, nestedIds, nullId]).toHaveLength(4);
524+
});
525+
});

packages/spec/src/ui/action-params.zod.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,49 @@ export function validateActionParams(
231231
* context-less, RLS/FLS-bypassing by design (#2849); the boundary is enforced
232232
* at invoke time (`ai.exposed` + the ADR-0066 D4 capability gate), not here.
233233
*
234-
* `find` is the one member whose argument shape the signature alone never
235-
* settled: it takes a bare FILTER — the `where` half of a query — and never
236-
* an ObjectQL query envelope; read its doc comment before writing a handler
237-
* or a test double against it (#14175).
234+
* Two members carry an argument contract the signature alone does not settle,
235+
* and both state it on the member: `find` takes a bare FILTER — the `where`
236+
* half of a query — and never an ObjectQL query envelope (#14175); `delete`
237+
* accepts a single id OR an array of them, both as declared contract, served
238+
* one row at a time (#15117). Read those doc comments before writing a handler
239+
* or a test double against either.
238240
*/
239241
export interface ActionEngineFacade {
240242
insert(object: string, data: Record<string, unknown>): Promise<{ id: string }>;
241243
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
242-
delete(object: string, id: string): Promise<void>;
244+
/**
245+
* Delete rows of `object` by id.
246+
*
247+
* ## Which convention is the contract: BOTH — one id, or an array of them
248+
*
249+
* `idOrIds` takes a SINGLE id or an ARRAY of ids, and both spellings are
250+
* contract, not a runtime tolerance a handler author has to discover by
251+
* reading `packages/runtime`. Deleting one row is `delete(object, id)`;
252+
* deleting a set is `delete(object, ids)` — a handler that already holds a
253+
* list does NOT have to unroll it into a loop to stay on the contract.
254+
*
255+
* What the array form is, exactly: a convenience over the SAME per-row path,
256+
* never a bulk or atomic delete. The `delete` arm of
257+
* `packages/runtime/src/action-execution.ts#buildActionEngineFacade`
258+
* normalises the argument to a list and issues one `ql.delete` per id, in
259+
* order, under the caller's execution context. There is no transaction
260+
* around the set: a failure part-way through leaves the ids before it
261+
* deleted and the ids after it untouched, and the rejection a caller sees is
262+
* the one that stopped it. An empty array deletes nothing and resolves.
263+
*
264+
* Until #15117 this slot declared `id: string` while the runtime had been
265+
* accepting both forms all along, described in a comment there as an
266+
* accident of the two handler suites that happened to use them. It was
267+
* neither: it is this declaration. The one first-party suite on the array
268+
* form (`examples/app-todo/src/actions/task.handlers.ts`) could only reach
269+
* it by hand-rolling a copy of this interface — the same hand-written-fake
270+
* pattern #14175 found on `find`, one member over — and that copy drifted,
271+
* exactly as a copy does. Widening the declaration is what retired it.
272+
*
273+
* Both accepted forms, and the argument types that stay refused, are pinned
274+
* in `action-params.test.ts`.
275+
*/
276+
delete(object: string, idOrIds: string | string[]): Promise<void>;
243277
/**
244278
* Read the rows of `object` that match `filter`.
245279
*

0 commit comments

Comments
 (0)