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
12 changes: 12 additions & 0 deletions .changeset/23736-record-id-deleted-when.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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<string, Record<string, unknown>>;

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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
Loading