From 116a3edc4305e51273f815629ff0a4300d03100e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 04:01:29 +0000 Subject: [PATCH] refactor(metadata-core): drop sys_view_definition's all-six apiMethods whitelist (#3026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3745 completed this object's boilerplate CRUD-five whitelist to all six primitives so its batch routes stopped 405-ing. A whitelist naming all six is equivalent to no whitelist — except it stops tracking primitives the enum grows later — so the #3543 audit rule ("delete the equivalent-to-open boilerplate") applies and the declaration goes. No behaviour change: `undefined` resolves to `unrestricted`, whose effective operation set is identical to `restricted` holding all six primitives. Removing it is safe HERE specifically because the object has no `managedBy`: `reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array `apiMethods`, so for a managed object an absent whitelist would take the managed-write backstop with it. That is why the RBAC objects reclaimed by #3745 keep their explicit arrays and this one does not — the two treatments are not inconsistent, they follow the presence of a backstop. The object's test is rewritten to guard the new shape in both directions (whitelist must stay absent; the resolver must report `unrestricted`) and to pin the derived verbs the contract grants it. Verified end-to-end against a real running server (showcase, seeded admin), not only by unit test: 14/14 checks over the live REST surface, including this object's deleteMany reaching the engine rather than the API gate, derived import/export on a whitelist-free object, a tightened object still answering 405, and apiEnabled:false still answering 404 ahead of the method gate. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CkdX2VCuKfcsFBbvtATe7V --- .../sys-view-definition-default-open.md | 19 +++++++ .../sys-view-definition.object.test.ts | 50 +++++++++++-------- .../src/objects/sys-view-definition.object.ts | 9 ++-- 3 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 .changeset/sys-view-definition-default-open.md diff --git a/.changeset/sys-view-definition-default-open.md b/.changeset/sys-view-definition-default-open.md new file mode 100644 index 0000000000..4f74499092 --- /dev/null +++ b/.changeset/sys-view-definition-default-open.md @@ -0,0 +1,19 @@ +--- +"@objectstack/metadata-core": patch +--- + +refactor(metadata-core): drop `sys_view_definition`'s all-six `apiMethods` whitelist (#3026) + +#3745 completed this object's boilerplate CRUD-five whitelist to all six +primitives so its batch routes stopped 405-ing. A whitelist naming all six is +equivalent to no whitelist — except it stops tracking primitives the enum grows +later — so the #3543 audit rule applies and the declaration is removed. + +No behaviour change: `undefined` resolves to `unrestricted`, whose effective +operation set is identical to `restricted` holding all six. + +Removing it is safe HERE specifically because the object has no `managedBy`: +`reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array +`apiMethods`, so for a managed object an absent whitelist would take the +managed-write backstop with it. That is why the RBAC objects reclaimed by #3745 +keep their explicit arrays and this one does not. diff --git a/packages/metadata-core/src/objects/sys-view-definition.object.test.ts b/packages/metadata-core/src/objects/sys-view-definition.object.test.ts index d0a0b5dfc3..02e5d3874b 100644 --- a/packages/metadata-core/src/objects/sys-view-definition.object.test.ts +++ b/packages/metadata-core/src/objects/sys-view-definition.object.test.ts @@ -1,42 +1,48 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * #3026 follow-up — `sys_view_definition` must expose the BATCH shape of the - * write verbs it already grants. + * #3026 follow-up — `sys_view_definition` is default-open, and the derivation + * grants it every operation including the batch routes. * - * Since the #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`, a - * boilerplate CRUD-five whitelist (`get,list,create,update,delete`) denies - * `/batch`, `createMany`, `updateMany` and `deleteMany` while leaving the same - * verbs open one record at a time. The companion fix that added the `bulk` - * primitive to explicitly-whitelisted objects covered `platform-objects` only, - * so this one — the sole object carrying a whitelist in `metadata-core` — kept - * the gap. + * The #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`, which turned + * this object's boilerplate CRUD-five whitelist into a silent denial of + * `/batch`, `createMany`, `updateMany` and `deleteMany` while the same verbs + * stayed open one record at a time. #3745 completed the whitelist to all six + * primitives; #3543's audit rule then applies — a whitelist naming all six is + * equivalent to no whitelist while NOT tracking future primitives — so the + * declaration is gone. * - * Unlike the RBAC objects reclaimed alongside it, this object has no - * `managedBy`, so the ADR-0103 D3 reconciliation never applies and deleting the - * whitelist would be behaviourally equivalent. It stays explicit on purpose: a - * metadata-plane object that is ALSO reachable through the generic data API is - * worth naming its exposed surface rather than inheriting whatever the - * primitive set grows into. + * Deleting it is safe HERE specifically because the object has no `managedBy`: + * `reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array + * `apiMethods`, so for a managed object an absent whitelist would take the + * managed-write backstop with it. That is why the RBAC objects reclaimed by + * #3745 keep their explicit arrays and this one does not. */ import { describe, expect, it } from 'vitest'; import { resolveEffectiveApiMethods, isApiOperationAllowed } from '@objectstack/spec/data'; import { SysViewDefinitionObject } from './sys-view-definition.object.js'; -describe('sys_view_definition — batch exposure (#3026 / #3391 P1 companion)', () => { - it('grants the bulk primitive alongside its single-record write verbs', () => { - expect(SysViewDefinitionObject.enable?.apiMethods).toContain('bulk'); - for (const verb of ['get', 'list', 'create', 'update', 'delete'] as const) { - expect(SysViewDefinitionObject.enable?.apiMethods, `must keep ${verb}`).toContain(verb); - } +describe('sys_view_definition — API exposure (#3026 / #3543 audit)', () => { + it('carries no whitelist, so the resolver reports unrestricted', () => { + // Regression guard both ways: re-adding a whitelist naming all six + // primitives is a no-op that stops tracking future ones, and any narrower + // whitelist silently closes routes that are open today. + expect(SysViewDefinitionObject.enable?.apiMethods).toBeUndefined(); + expect(resolveEffectiveApiMethods(SysViewDefinitionObject.enable).mode).toBe('unrestricted'); }); it('admits createMany / updateMany / deleteMany and /batch', () => { const eff = resolveEffectiveApiMethods(SysViewDefinitionObject.enable); - expect(eff.mode).toBe('restricted'); for (const child of ['create', 'update', 'delete'] as const) { expect(isApiOperationAllowed(eff, 'bulk', { bulkChild: child }), `batch ${child}`).toBe(true); } }); + + it('derives the data-portability verbs from the primitives', () => { + const eff = resolveEffectiveApiMethods(SysViewDefinitionObject.enable); + for (const op of ['import', 'export', 'upsert', 'aggregate'] as const) { + expect(isApiOperationAllowed(eff, op), op).toBe(true); + } + }); }); diff --git a/packages/metadata-core/src/objects/sys-view-definition.object.ts b/packages/metadata-core/src/objects/sys-view-definition.object.ts index 1c11b89022..8b0e6abe55 100644 --- a/packages/metadata-core/src/objects/sys-view-definition.object.ts +++ b/packages/metadata-core/src/objects/sys-view-definition.object.ts @@ -138,8 +138,11 @@ export const SysViewDefinitionObject = ObjectSchema.create({ trackHistory: true, searchable: false, apiEnabled: true, - // `bulk` = the batch shape of the verbs above; the gate is `bulk ∧ child` - // (#3391 P1), so omitting it 405s /batch and the *Many routes (#3026). - apiMethods: ['get', 'list', 'create', 'update', 'delete', 'bulk'], + // No `apiMethods` — default-open (#3543 audit). #3745 completed this + // object's whitelist to all six primitives, which is equivalent to no + // whitelist while NOT tracking future primitives. Unlike the RBAC objects + // reclaimed alongside it, this one has no `managedBy`, so there is no + // ADR-0103 D3 managed-write backstop that an explicit array keeps alive — + // nothing argues for keeping the declaration, so it goes. }, });