diff --git a/.changeset/23736-record-id-deleted-when.md b/.changeset/23736-record-id-deleted-when.md new file mode 100644 index 00000000000..ceab3a008be --- /dev/null +++ b/.changeset/23736-record-id-deleted-when.md @@ -0,0 +1,12 @@ +--- +'@objectstack/plugin-approvals': patch +--- + +fix(plugin-approvals): `sys_approval_request.record_id` declares that the record it names was deleted when `cancel_reason` is `record_deleted` + +Clause-②: no + +- **The pointer carries its own tombstone predicate.** `record_id` declares `referenceDeletedWhen: { field: 'cancel_reason', value: 'record_deleted' }` beside its `referenceVia: 'object_name'`. A request the platform cancelled because its record was deleted (`status: cancelled`, `cancel_reason: record_deleted`) therefore says, in the served object definition, that the pointer names a record that no longer exists. A renderer draws that option's label (`Related record deleted`) in place of a link, and reads nothing. +- **Nothing is accepted or answered differently.** The sibling is the existing `cancel_reason` select, which already holds `record_deleted`, so the spec's parse-time refusals of the key are satisfied by the shipped object. No export, type, route, stored value or request answer moves. + +A patch: one key on one platform object's served field definition. diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request-pointer-pair.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-request-pointer-pair.test.ts index 23af10da517..45f477552ff 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-request-pointer-pair.test.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-request-pointer-pair.test.ts @@ -1,6 +1,7 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; +import { ObjectSchema } from '@objectstack/spec/data'; import { SysApprovalRequest } from './sys-approval-request.object.js'; /** @@ -49,3 +50,30 @@ describe('sys_approval_request — declared pointer pair (#11386)', () => { expect(fields.object_name.required).toBe(true); }); }); + +/** + * The pointer's tombstone predicate — objectui#12489 ruling, letter A. + * + * When the platform voids a `pending` request because its record was deleted + * (`cancelled` + `cancel_reason: 'record_deleted'`), `record_id` names a record + * that no longer exists. `referenceDeletedWhen` is the object's own assertion of + * that, so a renderer draws the sibling option's label instead of a link to + * nothing. The spec refuses the declaration at parse unless the sibling is a + * single-value `select` holding the option, so the parse is half of the pin. + */ +describe('sys_approval_request — record_id tombstone predicate', () => { + const fields = SysApprovalRequest.fields as Record>; + + it('declares record_id as deleted when cancel_reason holds record_deleted', () => { + expect(fields.record_id.referenceDeletedWhen).toEqual({ field: 'cancel_reason', value: 'record_deleted' }); + }); + + it('names a sibling select that declares that option, and the object parses through ObjectSchema', () => { + expect(fields.cancel_reason.type).toBe('select'); + expect(fields.cancel_reason.options).toEqual( + expect.arrayContaining([expect.objectContaining({ value: 'record_deleted' })]), + ); + const parsed = ObjectSchema.safeParse(SysApprovalRequest); + expect(parsed.success, parsed.success ? '' : JSON.stringify(parsed.error.issues)).toBe(true); + }); +}); diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts index 9fa7608ae58..eb3148ea79d 100644 --- a/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts +++ b/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts @@ -218,6 +218,9 @@ export const SysApprovalRequest = ObjectSchema.create({ // dropped-deferral error, and `success: false` — and since commit 1cba33f16 the // loader also WARNS at load time, before the engine rejects the row. referenceVia: 'object_name', + // A `cancelled` request whose `cancel_reason` is `record_deleted` names a + // record the platform deleted: a renderer draws that option's label, not a link. + referenceDeletedWhen: { field: 'cancel_reason', value: 'record_deleted' }, }), submitter_id: Field.lookup('sys_user', {