You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 69c7cd6
Browse filesBrowse the repository at this point in the historyBrowse files
feat(objectql): restore a recycle-bin entry under its original ids and emit data.record.restored (#30237 PR 3) (#31220)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi1a44tVerUCrhBK4MJbjCZx)_
Part of #30237
Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: follow-up #30847
## 维护者速读
- 改了什么:回收站可以"恢复"了。`ObjectQL.restore(entryId)` 把一条回收站记录按原 id
写回(主记录先、明细随后),保留创建人和创建时间,不跑新建钩子;删除时被清空的引用在没人改过的前提下重新挂回;汇总字段重算;每行发一个
`data.record.restored` 事件;成功后这条回收站记录被删掉。spec 只动了您同意的范围:两个事件枚举加
`data.record.restored`,回收站常量注释和测试钉子翻转;按您 16:53Z 选的 C,新增第四个拒绝码
`RESTORE_PARENT_IN_RECYCLE_BIN`(码表、错误码台账各一行),ADR-0148 D2
加一条、Consequences 加一句。
- 为什么改:ADR-0148 的第三步(#30237 拆分的 PR 3)。删除进回收站(PR 1)、彻底删除/清空(PR
2)已上线,没有恢复,回收站就只进不出。
- 风险与回滚:恢复要么整条成功要么什么都不写:唯一值被占、母记录已彻底删除、母记录还在回收站、对象已卸载这四种情况一律 409
拒绝,记录留在回收站;其中"母记录还在回收站"会给出母记录所在条目的 id,先恢复它就能再恢复子记录。权限读取要等 PR
4,在那之前非系统调用者一律被安全插件拒绝,所以这个 PR 上线后普通用户还用不到恢复,只有系统上下文能用。回滚就是 revert 本 PR。
- 先看:`packages/objectql/src/recycle-bin/restore.ts`(`judgeRestore`
四个拒绝、`writeBack` 的事务与逐笔撤销);`docs/adr/0148-…md` 的 D2 与
Consequences;`packages/spec/src/data/recycle-bin.zod.ts`
码表;`packages/spec/src/api/events.zod.ts` 与 `realtime.zod.ts`(枚举加值)。
- 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run
src/recycle-bin/restore.test.ts`,应看到 26 个用例全过,含四个 409 和"不跑新建钩子"。
- 需要你做的:本 PR 改了 `docs/adr/`,属 Tier H:席位用 Fable 复核 spec、ADR
和实现拒绝的代码,ACCEPT 后请您亲手合并或入队。
## What changes
**`ObjectQL.restore(entryId, { context })`** (new,
`packages/objectql/src/recycle-bin/restore.ts`; the engine keeps a
delegation stub, so the facade gate holds with no new registry row):
- Reads the entry raw through the bin's driver, scoped to the caller's
organization, and answers `null` when no entry is visible or no bin is
registered. A malformed entry id is refused `400 VALIDATION_ERROR`
(`param: 'entryId'`), as `purge(entryId)` refuses one.
- Dispatches ONE `restore` operation on the business object of the
entry's master row, with no `ast` and no id, as `purge(entryId)` is
dispatched.
- **Judges before it writes** (reads only, as the caller). Each refusal
is `409` in the ADR-0112 envelope with `object`, `recordId`, `field`,
`entryId` where they apply, and writes nothing; the entry stays:
- `RESTORE_PACKAGE_UNINSTALLED`: an object of the entry is no longer
registered;
- `RESTORE_PARENT_PURGED`: a row's master-detail parent is neither live,
nor a row of this entry, nor held by another entry, or its object is no
longer registered (`parentObject`, `parentId`);
- `RESTORE_PARENT_IN_RECYCLE_BIN`: a row's master-detail parent is not
live but is held by another entry, as that entry's master or as a row of
its cascade (`parentObject`, `parentId`, `parentEntryId`). The parents
that are neither live nor in the entry are collected across every row
and field first (each probed once), then answered together: one indexed
master lookup per parent object, and for the rest (the cascade-row case,
`account ← contact ← task`: the task deleted alone, then the account) at
most ONE walk over the entries the caller can see by key (`keysetWalk`,
pages of 200), each snapshot searched, stopping once every parent is
answered. All on the refusal path only; a walk the store cut short
throws a store-defect error, not a refusal. Unlike the other three it is
not permanent: restoring the parent's entry first lets this one through,
which two tests drive end to end. It is answered only after every row
has been judged for the three permanent refusals, so it never hides one;
- `RESTORE_UNIQUE_VALUE_TAKEN`: a live row holds the id or a unique
value (`unique: true | 'organization'` scoped to the row's own
organization, `'global'` unscoped) of a snapshotted row. A value the
caller cannot see (a global unique across organizations) is caught by
the store's own unique constraint at the write, and answered with the
same code.
- **Writes back in one unit of work**: the rows raw through each
object's driver, the master first, under their original ids,
`created_by` / `created_at` as the snapshot holds them, no insert hook;
then the re-links; then the entry's removal (last, so a restore that did
not land keeps its entry). One transaction when every object involved is
stored by the bin's driver; otherwise each write already made is undone
when a later one fails, and the split is logged at `warn`.
- **Re-link (ADR-0148 D2, both arities per the #30577 review should-fix
1)** through `engine.update` with the restore's context, as the delete's
`set_null` clear went: a single-valued field while it still holds no
value; a multi-valued one while it no longer holds the member (the
member is appended).
- **Roll-ups** recompute from the rows that are back, one
`recomputeSummaries` call per object; a failure surfaces as
`SummaryRecomputeError` after the rows are back, as on insert.
- **Events**: one `data.record.restored` per row, master first, through
the engine's own `publishDataEvent` (credential mask, `internal`
omission and `organizationId` off the row, as every record event).
**Engine shape** (hand-over 6690307585, "How PR 2 shaped the engine"):
- `OperationContext['operation']` gains `'restore'`;
`ENGINE_OPERATION_VERBS.restore = false`, so `beforeRestore` /
`afterRestore` stay outside the hook namespace and `registerHook` is not
narrowed.
- `restore` joins `WRITE_EPOCH_OPERATIONS`.
- The #7809 pin (`engine-middleware-operation-vocabulary.test.ts`) holds
9 verbs; `restore` is a dispatched destructive verb, never with an AST,
object-level only.
- plugin-security is untouched: `DESTRUCTIVE_OPERATIONS` already refuses
`restore` fail-closed for every non-system caller. A new pin in
`security-plugin.test.ts` holds that for the engine's object-level
dispatch, for a caller holding every CRUD bit, both super-user bypasses
and both new grant keys, with no row read.
**Spec (consent records 6709046255, 「A 同意三处(推荐)」, and 6710220841, ruling
C 「C 新码,并入 PR 3(推荐)」)**, `Clause-②: yes (widening)`:
- `packages/spec/src/api/events.zod.ts`: `DataEventType` gains
`'data.record.restored'`; its docblock says four events and names
`RECORD_RESTORED_EVENT`.
- `packages/spec/src/api/realtime.zod.ts`: `RealtimeEventType` gains the
same value; its docblock lists it.
- `packages/spec/src/data/recycle-bin.zod.ts`: the "NAMED, NOT YET
EMITTED" comment flips to emitted; `recycle-bin.zod.test.ts`'s pin flips
to "a member of both enums".
- `packages/spec/src/api/events.test.ts`: the `DataEventType.options`
equality pin gains the value (seat ruling Q2, recorded on #30237, the
mechanical consequence of the consented enum change).
- `packages/spec/src/data/recycle-bin.zod.ts`:
`RECYCLE_BIN_RESTORE_REFUSAL_CODES` gains `parentInRecycleBin:
'RESTORE_PARENT_IN_RECYCLE_BIN'`; the doc comment says four codes, the
first three permanent for the entry, the fourth clearing once the
parent's entry is restored. `recycle-bin.zod.test.ts` pins four codes.
- `packages/spec/src/api/error-code-ledger.zod.ts`: the code joins the
`@objectstack/objectql` block (the block's comment says four);
`error-code-ledger.test.ts` pins the four values in order.
- Regenerated: `content/docs/references/api/events.mdx`, `realtime.mdx`,
`error-code-ledger.mdx`, `automation-api.mdx`, `contract.mdx`
(`gen:docs`). No export added or removed in spec.
**ADR (Tier H, ruling 6710220841):** `docs/adr/0148-…md` D2 gains a
fourth refusal bullet (the parent is in the bin; dated, citing the
ruling record), and Consequences gains one sentence: the fourth refusal
is not permanent. The status line, D5's reader table and the rest are
untouched.
## Acceptance notes
- **Parent in the bin (ruling C, 6710220841; replaces the seat's interim
Q1 = A):** answered by its own code `RESTORE_PARENT_IN_RECYCLE_BIN`, not
`RESTORE_PARENT_PURGED`. The interim answer is gone with no fallback; an
ablation that stamps the interim code back turns the refusal's tests
red.
- Known gap: ADR-0148 D5's reader table still lists the refusal codes in
words as "unique value taken, parent purged, package uninstalled". That
row is outside the ADR hunk ruling 6710220841 consented to, so this PR
leaves it (seat, contract review round 1).
- Contract review round 1 (6711252037), fixed in `511372165`: a parent
held as a cascade row of another entry was answered
`RESTORE_PARENT_PURGED` (must-fix); a parent in the bin could hide a
permanent refusal of a later row (should-fix); a parent whose object is
unregistered now answers `RESTORE_PARENT_PURGED`. The refusal codes and
409 stay literal stamps in `restore.ts`, the form
`check:error-status-conformance` reads (its assignment reader pairs
`err.code = '...'` with the status); a test pins the four codes and 409
by value against `RECYCLE_BIN_RESTORE_REFUSAL_CODES` / `_STATUS`.
- Contract review round 2 (6711739005), fixed in `82b042200`: the miss
path walked the bin once per (row, field); it now walks it at most once
per restore, pinned by a test that counts bin reads (3 for five lines
under one parent). An unregistered parent object's refusal now says the
object no longer exists, rather than "deleted for good".
- A failed restore outside a transaction that had already re-linked a
reference publishes two `data.record.updated` events for that row (the
re-link and its undo), since both go through `engine.update`; a comment
in `writeBack` says so.
- The re-link runs as the restoring caller, as the clear ran as the
deleting one. Until PR 4 only the system restores, so nothing can be
refused there today; PR 4 decides whether a restorer who may not edit
the referencing row re-links it.
- A composite unique index (`indexes[].unique`) is not probed before the
write; the store's constraint catches it at the write and it is answered
`RESTORE_UNIQUE_VALUE_TAKEN`, the unit undone.
- A plain (non master-detail) lookup in a restored row that points at a
row deleted since stays as the snapshot holds it; ADR-0148 names no
refusal for it.
- Not in this PR: the permission reader (PR 4), the REST doors (PR 5),
the Console (#30239), the skills text (#30847). `@objectstack/client`'s
`subscribeToRecords` still lists created/updated/deleted only (an
explicit list, not an exhaustive switch); whether it also subscribes to
`restored` by default is PR 5's / #30239's choice. `service-knowledge`
does not re-index a restored record; it listens to created/updated only.
- Dispatch-order conflict, resolved by the rules file: the order asked
for `@objectstack/spec` `minor`, but the claim declares
`Metadata-protocol: none`, and AGENTS.md step 3 /
`check-changeset-no-major` grade a `none` change `patch`. The changeset
is `patch` for both packages.
## Tests
All on head `82b042200` (the merge of `origin/main` 13c6d8f, the
ruling-C commit, and the contract-review round 1 and 2 fixes):
- `pnpm --filter @objectstack/objectql exec vitest run
src/recycle-bin/restore.test.ts`: Tests 31 passed (31). Round trip and
dispatch shape, `created_by` / `created_at` kept, no insert hook,
restored events, roll-up recompute, organization scoping, null and
bad-id answers, the security refusal and the system path, re-link for
both arities (changed since, row gone, the `relinkedValue` units), and
the refusals: `RESTORE_PACKAGE_UNINSTALLED`; `RESTORE_PARENT_PURGED`
(parent purged; parent object unregistered);
`RESTORE_PARENT_IN_RECYCLE_BIN` (parent the master of another entry;
grandchild whose parent is a cascade row of another entry, each refused,
then the parent's entry restored, then the row restores); five lines
under one parent held as a cascade row answered with 3 bin reads; a
permanent refusal (id taken) winning over a parent in the bin;
`RESTORE_UNIQUE_VALUE_TAKEN` (a value, a per-organization value in
another organization allowed, the id, and the store's own violation with
the undo); the four codes and 409 pinned by value against the spec
constants.
- Full `@objectstack/objectql` suite on `82b042200`: Test Files 566
passed (566), Tests 11458 passed (11458).
- On `ff9ad72fb` (the review fixes touch only `restore.ts` and its
test): full `@objectstack/plugin-security` Test Files 248 passed, Tests
5821 passed, 45 skipped, with the new pin `[#30237] DENIES the engine's
object-level restore dispatch fail-closed`; full `@objectstack/spec`
Test Files 911 passed, Tests 27487 passed, 1 todo.
**Ablations** on `82b042200` (`scripts/ablation-replace.mjs`, each
mutation proven on disk, then restored to the HEAD blob with an empty
`git diff HEAD`). Clean run: Tests 31 passed (31). Each mutated run:
| Mutation | Result |
|---|---|
| bin asked once per missing reference, not once for all | 1 failed, 30
passed: the bin-read count test (`expected 11 to be 3`) |
| bin lookup by master only (no snapshot walk) | 2 failed, 29 passed:
the grandchild and the read-count tests |
| first reason wins (in-bin thrown at the reference, not deferred) | 1
failed, 30 passed: `a permanent refusal wins over
RESTORE_PARENT_IN_RECYCLE_BIN` |
| unregistered parent object check off | 1 failed, 30 passed:
`RESTORE_PARENT_PURGED: a parent whose object is no longer registered` |
| purged-before-in-bin check off | 1 failed, 30 passed:
`RESTORE_PARENT_PURGED: a detail whose master was deleted for good` |
| unique-value probe off | 1 failed, 30 passed:
`RESTORE_UNIQUE_VALUE_TAKEN: a live row holds a unique value` |
| id probe off | 2 failed, 29 passed: `a live row holds the id` and the
permanent-first test |
| master-detail parent check off | 5 failed, 26 passed: every parent
test |
| interim code stamped back (`RESTORE_PARENT_PURGED` for the in-bin
case) | 3 failed, 28 passed: every `RESTORE_PARENT_IN_RECYCLE_BIN` test
|
| package check off | 1 failed, 30 passed: `RESTORE_PACKAGE_UNINSTALLED`
|
| raw create swapped for the engine's hooked `insert` (two files:
`restore.ts` plus an `insert` arrow held on the engine host) | 4 failed,
27 passed, among them `runs no insert hook` (received
`beforeInsert:account`, `afterInsert:account`, ...) |
| `'restore'` removed from plugin-security `DESTRUCTIVE_OPERATIONS`
(`vitest run src/security-plugin.test.ts -t 30237`, on `ff9ad72fb`) | 1
failed: `promise resolved ... instead of rejecting`; clean run 1 passed
|
## Verification
On head `82b042200`:
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 119 commands, each run and recorded as `CMD
:: exit N`; all 119 exit 0. `--ran`: 119 derived, 119 run, 0
NOT-MEASURED, 0 UNRUN.
- `pnpm --filter @objectstack/objectql run typecheck`: exit 0. ESLint on
`packages/objectql/src/recycle-bin/`: exit 0.
- On `ff9ad72fb`: `pnpm lint` exit 0; `@objectstack/plugin-security` and
`@objectstack/spec` typecheck exit 0; a whole-workspace `pnpm build`
(119 tasks) ran before the gate rounds, and `@objectstack/objectql` was
rebuilt for each later head.
- CI check-runs are not cited: this thread does not wait on CI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017ihL2fsWoxB3no3RuuUR8y
<!-- signed: domain:server#2 · marchtian ·
session_017ihL2fsWoxB3no3RuuUR8y -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(objectql): restore a recycle-bin entry under its original ids, and publish `data.record.restored` (ADR-0148 D2)
7
+
8
+
Clause-②: yes (widening)
9
+
Metadata-protocol: none
10
+
11
+
`ObjectQL.restore(entryId, { context })` writes one recycle-bin entry back: every row of the entry, the master first and its master-detail details after it, under the ids they had, with `created_by` / `created_at` as they were. No insert hook runs, no default or autonumber is issued: a restore is not a create. The references the delete cleared (`set_null` lookups) are re-linked where nothing has changed them since: a single-valued field while it still holds no value, a multi-valued one while it no longer holds the member. Roll-ups recompute from the rows that are back, one `data.record.restored` event is published per row, and the entry is removed. It answers what came back (`RecycleBinRestoreResult`: the entry, the master's object and id, every row, how many references were re-linked), or `null` when no entry with that id is visible to the caller's organization or the composition registers no bin.
12
+
13
+
The entry is the unit: every row comes back or none does. A restore that would write what it should not is refused `409` before anything is written, and the entry stays:
14
+
15
+
-`RESTORE_PACKAGE_UNINSTALLED`: an object of the entry is no longer registered.
16
+
-`RESTORE_PARENT_PURGED`: a row's master-detail parent is neither live, nor a row of the entry, nor the record of another recycle-bin entry.
17
+
-`RESTORE_PARENT_IN_RECYCLE_BIN`: a row's master-detail parent is not live but is the record of another recycle-bin entry. The refusal names that entry (`parentEntryId`, beside `parentObject` / `parentId`) and says to restore it first; unlike the other three, it clears once that entry is restored.
18
+
-`RESTORE_UNIQUE_VALUE_TAKEN`: a live row holds the id or a unique value of a snapshotted row, found by a probe before the write or by the store's own unique constraint at it.
19
+
20
+
The writes run in one transaction when every object involved is stored by the recycle bin's driver; otherwise each write already made is undone when a later one fails.
21
+
22
+
The restore is dispatched as the new `restore` operation on the business object, with no query AST and no id, like `purge(entryId)`. `beforeRestore` / `afterRestore` stay outside the engine's hook namespace, so registering them only warns, as before. `restore` advances the write epoch. Until `@objectstack/plugin-security` maps the verb to `allowRestoreOwnDeleted` / `manageRecycleBin`, its fail-closed backstop refuses every restore by a caller that is not the system.
23
+
24
+
`@objectstack/spec`: `DataEventType` and `RealtimeEventType` gain `data.record.restored` (`RECORD_RESTORED_EVENT`), now that the engine emits it. A pure widening: a consumer that matches the event names it knows keeps working, and one that wants restores subscribes to the new name. A consumer keyed on `data.record.created` does not see a restore.
25
+
26
+
`@objectstack/spec`: `RECYCLE_BIN_RESTORE_REFUSAL_CODES` gains `parentInRecycleBin` (`'RESTORE_PARENT_IN_RECYCLE_BIN'`), registered in the error-code ledger under `@objectstack/objectql`, 409 like the other three. Also a widening: a consumer that switches over the codes it knows keeps working and treats the new one as an unknown 409; one that wants to offer "restore the parent first" reads `parentEntryId`.
Copy file name to clipboardExpand all lines: content/docs/references/api/automation-api.mdx
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -417,7 +417,7 @@ const result = AutomationApiErrorCode.parse(data);
417
417
418
418
| Property | Type | Required | Description |
419
419
| :--- | :--- | :--- | :--- |
420
-
|**code**|`Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +362 more>`| ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
420
+
|**code**|`Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +363 more>`| ✅ | The inner refusal's code in its WIRE spelling - the ADR-0112 member a record save of the same write answers in `error.code` (e.g. `UNIQUE_VIOLATION`); the closed set `ApiErrorSchema.code` parses against |
421
421
|**message**|`string`| ✅ | The inner refusal's own diagnostic text, verbatim, without the node and operation prefixes the run's failure text adds |
422
422
423
423
### Allowed Values: `FlowInnerRefusal.code`
@@ -707,6 +707,7 @@ const result = AutomationApiErrorCode.parse(data);
Copy file name to clipboardExpand all lines: content/docs/references/api/contract.mdx
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);
28
28
29
29
| Property | Type | Required | Description |
30
30
| :--- | :--- | :--- | :--- |
31
-
|**code**|`Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +362 more>`| ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
31
+
|**code**|`Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +363 more>`| ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
32
32
|**declaredCode**|`string`| optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
|**userMessage**|`string`| optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -325,6 +325,7 @@ const result = ApiErrorSchema.parse(data);
|**organizationId**|`string`| optional | Organization the record belongs to (its organization_id), so a tenant-scoped consumer can discriminate the event's tenant without reading the record body. Absent when the record belongs to no organization: a row of an object outside the wall (environment-wide, or no organization column), and every row of a composition that registers no organization object. A single-posture deployment stamps every tenant row with its Default Organization, so its events carry it — read absence as "not behind any organization wall", never as "unknown". Present = exactly that organization; never fabricated, and the empty string is refused. |
@@ -76,6 +76,7 @@ const result = BulkDataEventSchema.parse(data);
Copy file name to clipboardExpand all lines: content/docs/references/api/realtime.mdx
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -71,6 +71,7 @@ Realtime event type: a record-change event name the runtime emits
71
71
*`data.record.created`
72
72
*`data.record.updated`
73
73
*`data.record.deleted`
74
+
*`data.record.restored`
74
75
*`data.records.updated`
75
76
*`data.records.deleted`
76
77
@@ -119,7 +120,7 @@ Realtime event type: a record-change event name the runtime emits
119
120
120
121
| Property | Type | Required | Description |
121
122
| :--- | :--- | :--- | :--- |
122
-
|**type**|`Enum<'data.record.created' \| 'data.record.updated' \| 'data.record.deleted' \| 'data.records.updated' \| 'data.records.deleted'>`| ✅ | Type of event to subscribe to |
123
+
|**type**|`Enum<'data.record.created' \| 'data.record.updated' \| 'data.record.deleted' \| 'data.record.restored' \| 'data.records.updated' \| 'data.records.deleted'>`| ✅ | Type of event to subscribe to |
123
124
|**object**|`string`| optional | Object name to subscribe to |
0 commit comments