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
5 changes: 5 additions & 0 deletions .changeset/inline-view-arms-require-object-binding.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@objectstack/spec": minor
---

The runtime metadata write door for `view` now refuses an inline view config that carries no object binding. The two flattened-overlay members of `ViewMetadataSchema` require `object` and `viewKind` — the pair every object-bound read path (`GET /meta/view?object=`, the view switcher) matches stored rows on — so a flat body like `{ name, type: 'grid', columns: [...] }` is rejected at save time (draft and active alike) with located guidance naming the missing binding and the `defineView({ list: { … } })` wrap remedy, instead of being stored, reported valid, and served by nothing. Console personalization PUTs are unaffected: the write path inherits the binding from the registry entry the overlay shadows before validation. Union membership is unchanged — four arms, same order, same JSON-Schema `anyOf` face; the inline arms' required set is the only change.
Original file line numberDiff line numberDiff line change
Expand Up@@ -189,8 +189,14 @@ describe('#5599 a stored `view` that is not a view is no longer badged valid', (

it('a legitimately-lean overlay is still valid — no collateral badge', () => {
// The precondition asks "is this a view at all", never "is it complete".
expect(computeMetadataDiagnostics('view', { isPinned: true })).toEqual({ valid: true });
expect(computeMetadataDiagnostics('view', { hidden: true })).toEqual({ valid: true });
// [#7741] "lean" now still carries the object binding: the write path
// inherits `object`/`viewKind` from the shadowed entry (#2555), so a
// stored lean overlay of a REAL view looks exactly like this.
expect(computeMetadataDiagnostics('view', { isPinned: true, object: 'task', viewKind: 'list' })).toEqual({ valid: true });
expect(computeMetadataDiagnostics('view', { hidden: true, object: 'task', viewKind: 'list' })).toEqual({ valid: true });
// …while a stored row with NO binding is a row no object-bound read
// path can serve — the #7741 dead row — and is badged invalid now.
expect(computeMetadataDiagnostics('view', { isPinned: true })?.valid).toBe(false);
});

it('a stored row of pure identity is no longer valid either', () => {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,6 +195,7 @@ const viewBody = (name: string) => ({
name,
label: 'Project Tasks',
object: 'proj_task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'title', label: 'Title' }],
});

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -198,6 +198,7 @@ const VIEW = {
name: 'probe_view',
label: 'Probe',
object: 'task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
};

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -330,6 +330,7 @@ describe('code-only metadata types are refused on every kernel (#5086)', () => {
name: 'rc3_probe_view',
label: 'Probe',
object: 'task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
},
});
Expand DownExpand Up@@ -452,6 +453,7 @@ describe('code-only metadata types are refused on every kernel (#5086)', () => {
name: 'rc3_receipt_view',
label: 'Receipt',
object: 'task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
},
},
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -178,6 +178,7 @@ const VIEW = {
name: 'overdue_grid',
label: 'Overdue',
object: 'task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
};

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,7 +201,8 @@ const save = (p: ObjectStackProtocolImplementation) =>
p.saveMetaItem({
type: 'view',
name: 'v1',
item: { name: 'v1', label: 'Edited', type: 'grid', columns: ['name'] },
// [#7741] carries the object binding the inline arm now requires.
item: { name: 'v1', label: 'Edited', type: 'grid', columns: ['name'], object: 'task', viewKind: 'list' },
} as any);

const remove = (p: ObjectStackProtocolImplementation) =>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -266,6 +266,7 @@ const VIEW = {
name: 'org_grid',
label: 'Org grid',
object: 'task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'title', label: 'Title' }],
};

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -390,7 +390,8 @@ describe('saveMetaItem canonicalizes flow bodies (#4542)', () => {
type: 'view',
name: 'case_grid',
organizationId: 'org_alpha',
item: { name: 'case_grid', type: 'grid', label: 'Cases', columns: ['id', 'title'] },
// [#7741] carries the object binding the inline arm now requires.
item: { name: 'case_grid', type: 'grid', label: 'Cases', columns: ['id', 'title'], object: 'case', viewKind: 'list' },
});
expect(spy).not.toHaveBeenCalled();
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -144,6 +144,7 @@ const VIEW = {
name: 'rc5_probe_view',
label: 'Probe',
object: 'task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
};

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,8 @@ describe('#5364 saveMetaItem 422 expands union branches', () => {
const { protocol, rows } = makeProtocol();

const result = await save(protocol, {
name: 'task_list', object: 'task', type: 'grid', label: 'Tasks',
// [#7741] carries the object binding the inline arm now requires.
name: 'task_list', object: 'task', viewKind: 'list', type: 'grid', label: 'Tasks',
columns: [{ field: 'title' }],
});

Expand Down
2 changes: 2 additions & 0 deletions packages/objectql/src/overlay-precedence.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,7 @@ const validView = {
name: 'case_grid',
label: 'Cases',
object: 'case',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [
{ field: 'name', label: 'Name' },
{ field: 'status', label: 'Status' },
Expand DownExpand Up@@ -401,6 +402,7 @@ describe('canonical hash stability (PR-10b backfill precondition)', () => {
it('hashSpec: stable across key reorder for a view payload', () => {
const reordered = {
columns: validView.columns,
viewKind: validView.viewKind, // [#7741] rides with the binding pair
object: validView.object,
label: validView.label,
name: validView.name,
Expand Down
1 change: 1 addition & 0 deletions packages/objectql/src/protocol-commit-history.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -397,6 +397,7 @@ function makeRealRepoHarness(seedCommits: any[] = [], opts: { controlPlane?: boo
*/
const gridBody = (label: string) => ({
name: 'myapp_case_grid', type: 'grid', label, columns: ['id', 'title'],
object: 'case', viewKind: 'list', // [#7741] the inline arm requires the object binding pair
});

/** v1 authored in the package workspace, then the edit the commit recorded. */
Expand Down
2 changes: 2 additions & 0 deletions packages/objectql/src/protocol-lock-enforcement.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,7 @@ function seedLockedArtifact(
name,
label: name,
object: 'case',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
_packageId: '@objectstack/test-fixture',
_packageVersion: '1.0.0',
Expand All@@ -74,6 +75,7 @@ const validView = {
name: 'case_grid',
label: 'Cases',
object: 'case',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
};

Expand Down
7 changes: 6 additions & 1 deletion packages/objectql/src/protocol-meta.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,8 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => {
name: 'test_grid',
type: 'grid',
label: 'Test Grid',
object: 'test_task', // [#7741] the inline arm requires the object binding pair
viewKind: 'list',
columns: ['id', 'title'],
};

Expand DownExpand Up@@ -420,6 +422,8 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => {
name: 'all_leads',
label: 'All Leads',
type: 'grid',
object: 'lead', // [#7741] the inline arm requires the object binding pair
viewKind: 'list',
data: { provider: 'object', object: 'lead' },
columns: ['first_name', 'last_name'],
};
Expand DownExpand Up@@ -1707,7 +1711,8 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => {
it('artifact-backed view (allowOrgOverride:true) still overlays cleanly', async () => {
// Regression: types that DO allow overlays must keep working
// even when the item is artifact-backed.
const viewBase = { name: 'case_grid', type: 'grid' as const, object: 'case', columns: [{ field: 'name' }] };
// [#7741] the inline arm requires the object binding pair.
const viewBase = { name: 'case_grid', type: 'grid' as const, object: 'case', viewKind: 'list' as const, columns: [{ field: 'name' }] };
registry.registerItem('view', viewBase, 'name' as any, 'crm-plugin');
mockEngine.findOne.mockResolvedValue(null);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,7 +575,7 @@ describe('ADR-0029 D9.9 / #6995 — the row\'s package_id is provenance, never a
const { protocol } = makeSession();
const res = await protocol.saveMetaItem({
type: 'view', name: 'shared_grid', packageId: OTHER_PKG,
item: { name: 'shared_grid', type: 'grid', columns: ['name'] },
item: { name: 'shared_grid', type: 'grid', columns: ['name'], object: 'task', viewKind: 'list' }, // [#7741] the inline arm requires the object binding pair
});
expect(res.success).toBe(true);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,6 +114,7 @@ const viewBody = (name: string, label: string) => ({
name,
label,
object: 'showcase_task',
viewKind: 'list', // [#7741] the inline arm requires the object binding pair
columns: [{ field: 'name', label: 'Name' }],
});

Expand Down
5 changes: 3 additions & 2 deletions packages/objectql/src/protocol-publish-rollback.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -163,6 +163,7 @@ function makeStubEngine() {
describe('publishMetaItem / rollbackMetaItem / diffMetaItem', () => {
const sampleBody = (label: string) => ({
name: 'case_grid', type: 'grid', label, columns: ['id', 'title'],
object: 'case', viewKind: 'list', // [#7741] the inline arm requires the object binding pair
});

it('saveMetaItem with mode=draft creates a draft row, active read sees published', async () => {
Expand DownExpand Up@@ -325,11 +326,11 @@ describe('publishMetaItem / rollbackMetaItem / diffMetaItem', () => {
const protocol = new ObjectStackProtocolImplementation(engine);
await protocol.saveMetaItem({
type: 'view', name: 'case_grid', organizationId: 'org_alpha',
item: { name: 'case_grid', type: 'grid', label: 'A', columns: ['id'] },
item: { name: 'case_grid', type: 'grid', label: 'A', columns: ['id'], object: 'case', viewKind: 'list' }, // [#7741] the inline arm requires the object binding pair
});
await protocol.saveMetaItem({
type: 'view', name: 'case_grid', organizationId: 'org_alpha',
item: { name: 'case_grid', type: 'grid', label: 'B', columns: ['id', 'title'], extra: 1 },
item: { name: 'case_grid', type: 'grid', label: 'B', columns: ['id', 'title'], extra: 1, object: 'case', viewKind: 'list' }, // [#7741] the inline arm requires the object binding pair
});
const diff = await protocol.diffMetaItem({
type: 'view', name: 'case_grid', organizationId: 'org_alpha',
Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/protocol-recorded-by-null.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -159,7 +159,7 @@ function makeStubDriver() {
return { driver, stores };
}

const viewBody = (label: string) => ({ name: 'cases', type: 'grid', label, columns: ['id']});
const viewBody = (label: string) => ({ name: 'cases', type: 'grid', label, columns: ['id'], object: 'case', viewKind: 'list' }); // [#7741] the inline arm requires the object binding pair

describe('#4556 — protocol write paths store NULL, not the sentinel string', () => {
let engine: ObjectQL;
Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/protocol-revert-org-scope.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ function makeStubDriver() {

const PKG = 'app.revertscope';
const ORG = 'org_x';
const viewBody = (label: string) => ({ name: 'cases', type: 'grid', label, columns: ['id']});
const viewBody = (label: string) => ({ name: 'cases', type: 'grid', label, columns: ['id'], object: 'case', viewKind: 'list' }); // [#7741] the inline arm requires the object binding pair

describe('#7559 — the revert reads the history row under the key the writer stored it with', () => {
let engine: ObjectQL;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,7 @@ function makeStubDriver() {
}

function viewBody(label: string) {
return { name: 'cases', type: 'grid', label, columns: ['id'] };
return { name: 'cases', type: 'grid', label, columns: ['id'], object: 'case', viewKind: 'list' }; // [#7741] the inline arm requires the object binding pair
}

describe('saveMetaItem — repository write path against real ObjectQL (PR-10d.4)', () => {
Expand Down
26 changes: 13 additions & 13 deletions packages/objectql/src/protocol-save-meta-repo-path.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,7 +104,7 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
type: 'view',
name: 'case_grid',
organizationId: 'org_alpha',
item: { name: 'case_grid', type: 'grid', label: 'Cases', columns: ['id', 'title'] },
item: { name: 'case_grid', type: 'grid', label: 'Cases', columns: ['id', 'title'], object: 'case', viewKind: 'list' },
});
expect(result.success).toBe(true);
expect((result as any).seq).toBeGreaterThan(0);
Expand All@@ -116,7 +116,7 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
it('repository path writes the checksum and surfaces seq', async () => {
const { engine, rows } = makeStubEngine();
const protocol = new ObjectStackProtocolImplementation(engine);
const body = { name: 'case_grid', type: 'grid', label: 'Cases', columns: ['id', 'title'] };
const body = { name: 'case_grid', type: 'grid', label: 'Cases', columns: ['id', 'title'], object: 'case', viewKind: 'list' };
const result = await protocol.saveMetaItem({
type: 'view',
name: 'case_grid',
Expand All@@ -134,11 +134,11 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
const protocol = new ObjectStackProtocolImplementation(engine);
const r1 = await protocol.saveMetaItem({
type: 'view', name: 'v', organizationId: 'org',
item: { name: 'view_one', type: 'grid', label: 'A', columns: ['id'] },
item: { name: 'view_one', type: 'grid', label: 'A', columns: ['id'], object: 'case', viewKind: 'list' },
});
const r2 = await protocol.saveMetaItem({
type: 'view', name: 'v', organizationId: 'org',
item: { name: 'view_one', type: 'grid', label: 'B', columns: ['id'] },
item: { name: 'view_one', type: 'grid', label: 'B', columns: ['id'], object: 'case', viewKind: 'list' },
});
expect((r1 as any).seq).toBe(1);
expect((r2 as any).seq).toBe(2);
Expand All@@ -152,13 +152,13 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
// First write establishes a HEAD.
await protocol.saveMetaItem({
type: 'view', name: 'v', organizationId: 'org',
item: { name: 'view_one', type: 'grid', label: 'A', columns: ['id'] },
item: { name: 'view_one', type: 'grid', label: 'A', columns: ['id'], object: 'case', viewKind: 'list' },
});
// Second write with an explicit stale parentVersion → conflict.
await expect(
protocol.saveMetaItem({
type: 'view', name: 'v', organizationId: 'org',
item: { name: 'view_one', type: 'grid', label: 'B', columns: ['id'] },
item: { name: 'view_one', type: 'grid', label: 'B', columns: ['id'], object: 'case', viewKind: 'list' },
parentVersion: 'sha256:notTheCurrentHead',
}),
).rejects.toMatchObject({
Expand All@@ -170,7 +170,7 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
it('repository path no-ops when body is identical (idempotent put)', async () => {
const { engine, rows } = makeStubEngine();
const protocol = new ObjectStackProtocolImplementation(engine);
const body = { name: 'view_one', type: 'grid', label: 'A', columns: ['id'] };
const body = { name: 'view_one', type: 'grid', label: 'A', columns: ['id'], object: 'case', viewKind: 'list' };
const r1 = await protocol.saveMetaItem({
type: 'view', name: 'v', organizationId: 'org', item: body,
});
Expand All@@ -189,11 +189,11 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
const protocol = new ObjectStackProtocolImplementation(engine);
await protocol.saveMetaItem({
type: 'view', name: 'v',
item: { name: 'view_one', type: 'grid', label: 'env-wide', columns: ['id'] },
item: { name: 'view_one', type: 'grid', label: 'env-wide', columns: ['id'], object: 'case', viewKind: 'list' },
});
await protocol.saveMetaItem({
type: 'view', name: 'v', organizationId: 'org_alpha',
item: { name: 'view_one', type: 'grid', label: 'org_alpha', columns: ['id'] },
item: { name: 'view_one', type: 'grid', label: 'org_alpha', columns: ['id'], object: 'case', viewKind: 'list' },
});
// Two rows: one with organization_id=null, one with org_alpha.
expect(rows.size).toBe(2);
Expand All@@ -210,7 +210,7 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
type: 'views',
name: 'case_grid',
organizationId: 'org',
item: { name: 'case_grid', type: 'grid', label: 'OK', columns: ['id'] },
item: { name: 'case_grid', type: 'grid', label: 'OK', columns: ['id'], object: 'case', viewKind: 'list' },
});
expect(result.success).toBe(true);
const row = Array.from(rows.values())[0];
Expand DownExpand Up@@ -255,7 +255,7 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
type: 'view',
name: 'cases',
organizationId: 'org_x',
item: { name: 'cases', type: 'grid', label: 'Original', columns: ['id'] },
item: { name: 'cases', type: 'grid', label: 'Original', columns: ['id'], object: 'case', viewKind: 'list' },
});
const beforeBody = (Array.from(rows.values())[0] as any).metadata;

Expand All@@ -265,7 +265,7 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
type: 'view',
name: 'cases',
organizationId: 'org_x',
item: { name: 'cases', type: 'grid', label: 'Mutated (should not land)', columns: ['id'] },
item: { name: 'cases', type: 'grid', label: 'Mutated (should not land)', columns: ['id'], object: 'case', viewKind: 'list' },
parentVersion: 'sha256:stale',
}),
).rejects.toMatchObject({ code: 'METADATA_CONFLICT', status: 409 });
Expand DownExpand Up@@ -378,7 +378,7 @@ describe('saveMetaItem — repository write path (post PR-10d.6)', () => {
organizationId: 'org_alpha',
packageId: 'app.objectstack.hotcrm',
mode: 'draft',
item: { name: 'case_grid', type: 'grid', label: 'Cases (org overlay)', columns: ['id', 'title'] },
item: { name: 'case_grid', type: 'grid', label: 'Cases (org overlay)', columns: ['id', 'title'], object: 'case', viewKind: 'list' },
});
const create = inserts.find((d) => d.type === 'view' && d.name === 'case_grid');
expect(create).toBeTruthy();
Expand Down
Loading
Loading