Skip to content

Commit 57b0e5a

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24648-etag-describe
2 parents bbe6b22 + 92ce3af commit 57b0e5a

20 files changed

Lines changed: 771 additions & 51 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
'@objectstack/plugin-audit': patch
4+
'@objectstack/plugin-sharing': patch
5+
---
6+
7+
fix(plugin-approvals, plugin-audit, plugin-sharing): list views no longer show the target object's API name as an Object column
8+
9+
Clause-②: no
10+
11+
- **The Object column leaves these lists.** These list views showed `object_name`, the target object's API name (such as `showcase_announcement`), as plain text beside the `record_id` record pointer. They no longer list it:
12+
- `sys_approval_request`: `all_requests`, `my_pending`, `submitted_by_me` and `completed`;
13+
- `sys_approval_action`: `recent` and `all_actions`;
14+
- `sys_audit_log`: `recent`, `writes_only`, `record_views` and `all_events`;
15+
- `sys_record_share`: all six views;
16+
- `sys_share_link`: all four views.
17+
- **The Record column names the target record.** `record_id` is the id half of the `record_id` / `object_name` pointer pair. The console draws it as the target record, with its object's icon and the record's name, and fetches `object_name` for that cell without a column. This relies on a console that fetches a pointer's type half for its cell, as the console bundled with this release does; an older console draws the raw record id in that column.
18+
- **Unchanged:** `object_name` stays a declared field on all five objects, and the pointer pair, its indexes and every query on it are untouched. `sys_audit_log`'s `config_changes` view keeps its Object column, because its rows carry no `record_id`.
19+
20+
A patch: entries removed from list view `columns` on shipped platform objects. No export, type, accepted value, served key or stored column changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
---
4+
5+
fix(service-automation)!: a `runAs: 'user'` hook fired by a user-triggered `runAs: 'system'` flow's write runs on that user's own grants
6+
7+
Clause-②: no (narrowing) — a `runAs: 'user'` hook fired by a write of a `runAs: 'system'` flow that names a user now runs on that user's own grants instead of the member baseline, so for a user whose own grants admit less than the baseline its reads narrow; the published `RunIdentityContext` / `RunDataContext` types make `positions` and `permissions` optional, and no published schema or export moves.
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A runtime semantics change of the data context a runAs 'system' flow run hands ObjectQL when it names a user, not a metadata shape change. No spec key, enum value, export, option, config field, response field or stored shape is removed, renamed or re-shaped, so there is no tombstone and nothing for `objectstack migrate meta` to rewrite. What narrows is whose grants a runAs 'user' hook fired by such a run's write carries: the named user's own, resolved in the run's organization, instead of the member baseline, so for a user whose own grants admit less than the baseline that hook reads less. The run's own operations keep full system privileges. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this behaviour and this diff adds none (not registered / already-registered); and the change is a behaviour change, not only an interface or type change (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
A `runAs: 'system'` flow run that names a user (a manual run, a record change fired by a user's write, and every post-decision node of a `runAs: 'system'` approval flow, where the user is the decider) handed ObjectQL `{ isSystem: true, userId, tenantId, positions: [], permissions: [] }`. Its own operations never read the two arrays: elevation short-circuits every gate before they are read. But a `runAs: 'user'` hook fired by one of its writes de-elevates to that context, and the engine keeps the arrays a context carries, resolving the user's grants only when none are carried. So that hook ran on the member baseline, whoever the user was.
14+
15+
- **Now:** a `runAs: 'system'` run that names a user carries no `positions` or `permissions` in its data context. A `runAs: 'user'` hook its writes fire resolves the named user's own positions, permission sets and organization peers in the run's organization, exactly as that user's own request resolves them. After an approval decision that user is the decider, so the hooks the post-decision writes fire run on the decider's grants.
16+
- **Wider:** for a user granted more than the baseline, such a hook now reads and writes what the user may, where it was refused on objects the baseline does not grant.
17+
- **Narrower:** for a user whose own grants admit less than the baseline, such a hook now reads only what that user's own request reads.
18+
- **Unchanged:** the run's own elevation, `isSystem`, its `actor` label, its `userId` and its `tenantId`; a user-less `runAs: 'system'` run (a schedule), which keeps `positions: []` and `permissions: []`, and a `runAs: 'user'` hook it fires is still refused for want of a user; hooks declaring `runAs: 'system'` or `'inherit'`; and `runAs: 'user'` flows.
19+
20+
The published `RunIdentityContext` and `RunDataContext` types of `@objectstack/service-automation` declare `positions` and `permissions` optional, because a `runAs: 'system'` envelope that names a user no longer carries them. Code that reads either member from `resolveRunDataContext`'s answer must allow for its absence.
21+
22+
What changes for you: a `runAs: 'user'` hook that a user-triggered `runAs: 'system'` flow fires now sees exactly what that user sees. If such a hook must read or write something the user may not, declare it `runAs: 'system'`.

‎packages/plugins/plugin-approvals/src/approval-list-display-labels.integration.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,10 @@ describe('#23889 — the approval lists read engine-written display columns', ()
288288
for (const col of view.columns) expect(act.fields[col], `${name}.${col}`).toBeDefined();
289289
}
290290
for (const name of ['recent', 'all_actions']) {
291-
expect(act.listViews[name].columns, name).toEqual(expect.arrayContaining(['object_name', 'record_id', 'acted_as_label']));
291+
// The pointer shows as its id half; the type half is fetched for that
292+
// cell, never shown as a column (`sys-approval-request-pointer-pair.test.ts`).
293+
expect(act.listViews[name].columns, name).toEqual(expect.arrayContaining(['record_id', 'acted_as_label']));
294+
expect(act.listViews[name].columns, name).not.toContain('object_name');
292295
expect(act.listViews[name].columns, name).not.toContain('acted_as');
293296
}
294297
expect(act.fields.record_id.referenceVia).toBe('object_name');

‎packages/plugins/plugin-approvals/src/restored-approval-continuation.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ describe('ruling A — a decision resumes the run as the person who decided', ()
981981
} as never);
982982
expect(seen.before, 'the nodes before the decision run as the submitter').toEqual(flow === 'who_flow'
983983
? { userId: 'submitter', permissions: ['deal_rep'] }
984-
: { userId: 'submitter', permissions: [], isSystem: true });
984+
: { userId: 'submitter', isSystem: true });
985985
return (await data.find('sys_approval_request', { where: { status: 'pending' } }))[0];
986986
}
987987

@@ -998,7 +998,7 @@ describe('ruling A — a decision resumes the run as the person who decided', ()
998998
const req = await parked(automation, 'who_flow_sys');
999999
const out = await service.decide(req.id, { decision, actorId: 'u1', comment: decision }, DECIDER_CTX);
10001000
expect(out.resumed, JSON.stringify(out)).toBe(true);
1001-
expect(seen[`after_${decision}`]).toEqual({ userId: 'u1', permissions: [], isSystem: true });
1001+
expect(seen[`after_${decision}`]).toEqual({ userId: 'u1', isSystem: true });
10021002
});
10031003

10041004
it('CONTROL: an SLA auto-decision names no decider, so the run keeps the identity it had', async () => {

‎packages/plugins/plugin-approvals/src/sys-approval-action.object.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,17 @@ export const SysApprovalAction = ObjectSchema.create({
4747
// (`object_name` + `record_id`) are written by `ApprovalService` with the row,
4848
// copied from the request it belongs to (`actionRow`). The row is
4949
// append-only, so each is the engine's fact at the moment of the action; a
50-
// list shows them as stored and never re-derives them.
50+
// list shows them as stored and never re-derives them. The pointer shows as
51+
// its id half, `record_id`, which the console draws as the target record and
52+
// resolves through `object_name` without a column (objectui#13036); no list
53+
// view shows the type half (`sys-approval-request-pointer-pair.test.ts`).
5154
listViews: {
5255
recent: {
5356
type: 'grid',
5457
name: 'recent',
5558
label: 'Recent',
5659
data: { provider: 'object', object: 'sys_approval_action' },
57-
columns: ['created_at', 'object_name', 'record_id', 'request_id', 'step_label', 'action', 'actor_id', 'acted_as_label', 'via_override', 'comment'],
60+
columns: ['created_at', 'record_id', 'request_id', 'step_label', 'action', 'actor_id', 'acted_as_label', 'via_override', 'comment'],
5861
sort: [{ field: 'created_at', order: 'desc' }],
5962
pagination: { pageSize: 50 },
6063
emptyState: { title: 'No approval actions yet', message: 'Actions are logged automatically when approvals progress.' },
@@ -74,7 +77,7 @@ export const SysApprovalAction = ObjectSchema.create({
7477
name: 'all_actions',
7578
label: 'All',
7679
data: { provider: 'object', object: 'sys_approval_action' },
77-
columns: ['created_at', 'object_name', 'record_id', 'request_id', 'step_label', 'action', 'actor_id', 'acted_as_label', 'via_override', 'comment'],
80+
columns: ['created_at', 'record_id', 'request_id', 'step_label', 'action', 'actor_id', 'acted_as_label', 'via_override', 'comment'],
7881
sort: [{ field: 'created_at', order: 'desc' }],
7982
pagination: { pageSize: 100 },
8083
},

‎packages/plugins/plugin-approvals/src/sys-approval-request-pointer-pair.test.ts‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { describe, it, expect } from 'vitest';
44
import { ObjectSchema } from '@objectstack/spec/data';
5+
import { ApprovalsServicePlugin } from './approvals-plugin.js';
56
import { SysApprovalRequest } from './sys-approval-request.object.js';
67

78
/**
@@ -77,3 +78,64 @@ describe('sys_approval_request — record_id tombstone predicate', () => {
7778
expect(parsed.success, parsed.success ? '' : JSON.stringify(parsed.error.issues)).toBe(true);
7879
});
7980
});
81+
82+
/**
83+
* [#24501] A list names a pointer's target with the id half alone. The console
84+
* draws a `referenceVia` column as the target record (its object's icon and
85+
* name) and fetches the type half for that cell without a column
86+
* (objectui#13036); a type-half column draws the stored API name as text.
87+
* Enumerated over every object the plugin registers, so a view added later is
88+
* judged without anyone listing it here.
89+
*/
90+
describe('approvals list views — a pointer shows as its id half, never its type half (#24501)', () => {
91+
type PointerObject = {
92+
name: string;
93+
fields: Record<string, { referenceVia?: unknown } | undefined>;
94+
listViews?: Record<string, { columns?: ReadonlyArray<string | { field: string }> }>;
95+
};
96+
97+
async function registeredObjects(): Promise<PointerObject[]> {
98+
const registered: Array<{ objects: PointerObject[] }> = [];
99+
const ctx: any = {
100+
getService: (name: string) =>
101+
name === 'manifest' ? { register: (m: { objects: PointerObject[] }) => registered.push(m) } : undefined,
102+
logger: { info: () => {}, warn: () => {} },
103+
};
104+
await new ApprovalsServicePlugin({ disableService: true }).init(ctx);
105+
expect(registered).toHaveLength(1);
106+
return registered[0].objects;
107+
}
108+
109+
/** `object.view: column` for each shown id half, and for each type half shown beside one. */
110+
function pointerColumns(objects: readonly PointerObject[]): { idHalves: string[]; typeHalves: string[] } {
111+
const idHalves: string[] = [];
112+
const typeHalves: string[] = [];
113+
for (const object of objects) {
114+
for (const [view, def] of Object.entries(object.listViews ?? {})) {
115+
const shown = new Set((def.columns ?? []).map((c) => (typeof c === 'string' ? c : c.field)));
116+
for (const column of shown) {
117+
const via = object.fields[column]?.referenceVia;
118+
if (typeof via !== 'string') continue;
119+
idHalves.push(`${object.name}.${view}: ${column}`);
120+
if (shown.has(via)) typeHalves.push(`${object.name}.${view}: ${via}`);
121+
}
122+
}
123+
}
124+
return { idHalves, typeHalves };
125+
}
126+
127+
it('no list view shows a referenceVia sibling beside its id half', async () => {
128+
expect(pointerColumns(await registeredObjects()).typeHalves).toEqual([]);
129+
});
130+
131+
it('the request and action lists still show the id half, so the Record column names the target', async () => {
132+
expect(pointerColumns(await registeredObjects()).idHalves).toEqual(expect.arrayContaining([
133+
'sys_approval_request.all_requests: record_id',
134+
'sys_approval_request.my_pending: record_id',
135+
'sys_approval_request.submitted_by_me: record_id',
136+
'sys_approval_request.completed: record_id',
137+
'sys_approval_action.recent: record_id',
138+
'sys_approval_action.all_actions: record_id',
139+
]));
140+
});
141+
});

‎packages/plugins/plugin-approvals/src/sys-approval-request.object.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,20 @@ export const SysApprovalRequest = ObjectSchema.create({
9191
// reads them as stored and never re-derives a label from flow metadata or
9292
// resolves an approver address itself; `process_name`, `current_step` and
9393
// `pending_approvers` keep the machine values every query and decision reads.
94+
//
95+
// The target is the `record_id` column alone. It is the pointer's id half,
96+
// so the console draws the target record (its object's icon and name) and
97+
// fetches the type half `object_name` for that cell without a column
98+
// (objectui#13036). An `object_name` column drew the stored API name as
99+
// text; no list view shows a pointer's type half
100+
// (`sys-approval-request-pointer-pair.test.ts`).
94101
listViews: {
95102
all_requests: {
96103
type: 'grid',
97104
name: 'all_requests',
98105
label: 'All',
99106
data: { provider: 'object', object: 'sys_approval_request' },
100-
columns: ['process_label', 'object_name', 'record_id', 'status', 'current_step_label', 'pending_approver_labels', 'submitter_id', 'updated_at'],
107+
columns: ['process_label', 'record_id', 'status', 'current_step_label', 'pending_approver_labels', 'submitter_id', 'updated_at'],
101108
sort: [{ field: 'updated_at', order: 'desc' }],
102109
pagination: { pageSize: 50 },
103110
},
@@ -106,7 +113,7 @@ export const SysApprovalRequest = ObjectSchema.create({
106113
name: 'my_pending',
107114
label: 'My Pending',
108115
data: { provider: 'object', object: 'sys_approval_request' },
109-
columns: ['process_label', 'object_name', 'record_id', 'current_step_label', 'pending_approver_labels', 'submitter_id', 'updated_at'],
116+
columns: ['process_label', 'record_id', 'current_step_label', 'pending_approver_labels', 'submitter_id', 'updated_at'],
110117
filter: [
111118
{ field: 'status', operator: 'equals', value: 'pending' },
112119
{ field: 'pending_approvers', operator: 'contains', value: '{current_user_id}' },
@@ -120,7 +127,7 @@ export const SysApprovalRequest = ObjectSchema.create({
120127
name: 'submitted_by_me',
121128
label: 'I Submitted',
122129
data: { provider: 'object', object: 'sys_approval_request' },
123-
columns: ['process_label', 'object_name', 'record_id', 'status', 'current_step_label', 'updated_at'],
130+
columns: ['process_label', 'record_id', 'status', 'current_step_label', 'updated_at'],
124131
filter: [{ field: 'submitter_id', operator: 'equals', value: '{current_user_id}' }],
125132
sort: [{ field: 'updated_at', order: 'desc' }],
126133
pagination: { pageSize: 25 },
@@ -130,7 +137,7 @@ export const SysApprovalRequest = ObjectSchema.create({
130137
name: 'completed',
131138
label: 'Completed',
132139
data: { provider: 'object', object: 'sys_approval_request' },
133-
columns: ['process_label', 'object_name', 'record_id', 'status', 'submitter_id', 'completed_at'],
140+
columns: ['process_label', 'record_id', 'status', 'submitter_id', 'completed_at'],
134141
// Every terminal state, `cancelled` included (#13568) — a platform-voided
135142
// request is kept as audit evidence, and a terminal state absent from the
136143
// only curated terminal view is evidence nobody can find.

‎packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,20 @@ export const SysAuditLog = ObjectSchema.create({
4444
titleFormat: '{action} · {object_name}',
4545
highlightFields: ['created_at', 'action', 'object_name', 'record_id', 'user_id'],
4646

47+
// A list names the target with the pointer's id half, `record_id`: the
48+
// console draws it as the target record and fetches the type half
49+
// `object_name` for that cell without a column (objectui#13036). A list
50+
// view never shows the type half beside it, which would draw the stored API
51+
// name as text (`sys-audit-log-pointer-pair.test.ts`). `config_changes`
52+
// keeps `object_name`: its rows carry no `record_id`, so no pointer names
53+
// their target.
4754
listViews: {
4855
recent: {
4956
type: 'grid',
5057
name: 'recent',
5158
label: 'Recent',
5259
data: { provider: 'object', object: 'sys_audit_log' },
53-
columns: ['created_at', 'action', 'object_name', 'record_id', 'user_id'],
60+
columns: ['created_at', 'action', 'record_id', 'user_id'],
5461
sort: [{ field: 'created_at', order: 'desc' }],
5562
pagination: { pageSize: 50 },
5663
emptyState: { title: 'No audit events', message: 'Activity will appear here as users interact with the platform.' },
@@ -60,7 +67,7 @@ export const SysAuditLog = ObjectSchema.create({
6067
name: 'writes_only',
6168
label: 'Writes',
6269
data: { provider: 'object', object: 'sys_audit_log' },
63-
columns: ['created_at', 'action', 'object_name', 'record_id', 'user_id'],
70+
columns: ['created_at', 'action', 'record_id', 'user_id'],
6471
// `restore` removed (#8315): the value is retired from the enum, so the
6572
// filter would have matched nothing for the rest of time. The three that
6673
// remain are exactly what `actionFor()` in `audit-writers.ts` can emit.
@@ -105,7 +112,7 @@ export const SysAuditLog = ObjectSchema.create({
105112
name: 'record_views',
106113
label: 'Record Views',
107114
data: { provider: 'object', object: 'sys_audit_log' },
108-
columns: ['created_at', 'user_id', 'object_name', 'record_id', 'actor'],
115+
columns: ['created_at', 'user_id', 'record_id', 'actor'],
109116
filter: [{ field: 'action', operator: 'in', value: ['read'] }],
110117
sort: [{ field: 'created_at', order: 'desc' }],
111118
pagination: { pageSize: 50 },
@@ -137,7 +144,7 @@ export const SysAuditLog = ObjectSchema.create({
137144
name: 'all_events',
138145
label: 'All',
139146
data: { provider: 'object', object: 'sys_audit_log' },
140-
columns: ['created_at', 'action', 'object_name', 'record_id', 'user_id'],
147+
columns: ['created_at', 'action', 'record_id', 'user_id'],
141148
sort: [{ field: 'created_at', order: 'desc' }],
142149
pagination: { pageSize: 100 },
143150
},

0 commit comments

Comments
 (0)