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
19 changes: 19 additions & 0 deletions .changeset/sys-view-definition-default-open.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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.
},
});
Loading