@object-ui/data-objectstack 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +2311 -0
  2. package/README.md +557 -11
  3. package/dist/index.cjs +1663 -463
  4. package/dist/index.d.cts +1780 -518
  5. package/dist/index.d.ts +1780 -518
  6. package/dist/index.js +1648 -457
  7. package/package.json +9 -9
  8. package/dist/tsconfig.tsbuildinfo +0 -1
  9. package/src/aggregate-capability.test.ts +0 -177
  10. package/src/apiAccessDenied.test.ts +0 -233
  11. package/src/appAccessProbe.test.ts +0 -147
  12. package/src/bulkDelete.test.ts +0 -75
  13. package/src/bulkUpdate.test.ts +0 -93
  14. package/src/cache/MetadataCache.test.ts +0 -477
  15. package/src/cache/MetadataCache.ts +0 -289
  16. package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
  17. package/src/connection.test.ts +0 -188
  18. package/src/contracts.ts +0 -115
  19. package/src/deleteView.homes.test.ts +0 -389
  20. package/src/deleteViewContract.types.test.ts +0 -246
  21. package/src/errors.test.ts +0 -426
  22. package/src/errors.ts +0 -293
  23. package/src/exportDownload.test.ts +0 -141
  24. package/src/filter-entry-translation.test.ts +0 -360
  25. package/src/filter-operator-ast-parity.test.ts +0 -135
  26. package/src/getObjectSchema.test.ts +0 -145
  27. package/src/httpFailureLogging.test.ts +0 -109
  28. package/src/importJob.test.ts +0 -71
  29. package/src/index.ts +0 -4442
  30. package/src/integration.ts +0 -200
  31. package/src/listViewOverrides.test.ts +0 -241
  32. package/src/listViews.test.ts +0 -175
  33. package/src/metadata-client.get-envelope.test.ts +0 -171
  34. package/src/metadata-client.saveAdvisories.test.ts +0 -226
  35. package/src/metadata-client.test.ts +0 -303
  36. package/src/metadata-client.ts +0 -1046
  37. package/src/occ.test.ts +0 -101
  38. package/src/onMutation.test.ts +0 -589
  39. package/src/onSaveAdvisory.test.ts +0 -311
  40. package/src/onWriteWarning.test.ts +0 -273
  41. package/src/orderby-serialization.test.ts +0 -115
  42. package/src/queryDataset.test.ts +0 -543
  43. package/src/searchAll.test.ts +0 -120
  44. package/src/security.ts +0 -243
  45. package/src/spec-symbol-batch6.test.ts +0 -326
  46. package/src/studio.ts +0 -152
  47. package/src/updateView.draft.test.ts +0 -242
  48. package/src/userState.test.ts +0 -315
  49. package/src/userState.ts +0 -238
  50. package/src/v3-compat.test.ts +0 -229
  51. package/src/validation-error.test.ts +0 -94
  52. package/src/viewCacheInvalidation.pin.test.ts +0 -260
  53. package/src/viewInvalidationSeam.pin.test.ts +0 -162
@@ -1,171 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * objectui#4271 — `MetadataClient.get()` honors its documented unwrapped-body
11
- * contract.
12
- *
13
- * The framework answers `GET /meta/:type/:name` with the spec-declared
14
- * envelope `{ type, name, item, ...protection fields }`
15
- * (`GetMetaItemResponseSchema`; objectstack#5563 collapsed the read to that ONE
16
- * shape). `get()` used to hand that envelope straight back while its own
17
- * docblock promised the unwrapped body — so every consumer reading `obj.fields`
18
- * saw `undefined`, which is how the whole field half of the permission matrix
19
- * went dead for every object.
20
- *
21
- * The asymmetry that IS real and stays: `getDraft()` documents the envelope and
22
- * its callers read `.item`, so it must keep returning what the server sent.
23
- * A11/A10 below are the pair that pins both halves.
24
- */
25
-
26
- import { describe, it, expect, vi } from 'vitest';
27
- import { MetadataClient, type MetadataError } from './metadata-client';
28
-
29
- function mockFetch(handler: (url: string, init?: RequestInit) => Promise<Response>): typeof fetch {
30
- return vi.fn(handler) as unknown as typeof fetch;
31
- }
32
-
33
- function jsonResponse(body: unknown, init?: ResponseInit): Response {
34
- return new Response(JSON.stringify(body), {
35
- status: 200,
36
- headers: { 'content-type': 'application/json' },
37
- ...init,
38
- });
39
- }
40
-
41
- /** A client whose single-item read answers `body`, recording the URLs it hit. */
42
- function clientAnswering(body: unknown, init?: ResponseInit) {
43
- const urls: string[] = [];
44
- const c = new MetadataClient({
45
- baseUrl: 'http://localhost:3000',
46
- fetch: mockFetch(async (url) => {
47
- urls.push(url);
48
- return init ? new Response(JSON.stringify(body), init) : jsonResponse(body);
49
- }),
50
- });
51
- return { c, urls };
52
- }
53
-
54
- /** The card's live shape: 21 fields under `item`, as showcase_project answers. */
55
- const OBJECT_BODY = {
56
- name: 'showcase_project',
57
- label: 'Project',
58
- fields: { name: { type: 'text' }, stage: { type: 'select' } },
59
- };
60
- const ENVELOPE = { type: 'object', name: 'showcase_project', item: OBJECT_BODY };
61
-
62
- describe('objectui#4271 · MetadataClient.get() unwraps the spec envelope', () => {
63
- it('A1: unwraps `{type,name,item}` to the item body', async () => {
64
- const { c } = clientAnswering(ENVELOPE);
65
- const obj = await c.get<Record<string, unknown>>('object', 'showcase_project');
66
- expect(obj).toEqual(OBJECT_BODY);
67
- // The headline consequence: the field map is reachable again.
68
- expect(Object.keys((obj as any)?.fields ?? {})).toEqual(['name', 'stage']);
69
- });
70
-
71
- it('A2: unwraps when the ADR-0008 protection carriers ride along', async () => {
72
- // `GetMetaItemResponseSchema` spreads MetadataProtectionEnvelopeFields, so
73
- // a real envelope carries more than three keys. Unwrapping must not be
74
- // conditioned on the key COUNT.
75
- const { c } = clientAnswering({
76
- ...ENVELOPE,
77
- lock: { locked: false, reason: 'ok' },
78
- provenance: { origin: 'package' },
79
- });
80
- expect(await c.get('object', 'showcase_project')).toEqual(OBJECT_BODY);
81
- });
82
-
83
- it('A3: an envelope carrying a null item reads as null', async () => {
84
- const { c } = clientAnswering({ type: 'object', name: 'ghost', item: null });
85
- expect(await c.get('object', 'ghost')).toBeNull();
86
- });
87
- });
88
-
89
- describe('objectui#4271 · non-envelope responses pass through UNCHANGED', () => {
90
- it('A4: a bare body (older server / already-unwrapped) is returned as-is', async () => {
91
- const { c } = clientAnswering(OBJECT_BODY);
92
- expect(await c.get('object', 'showcase_project')).toEqual(OBJECT_BODY);
93
- });
94
-
95
- it('A5: type+name WITHOUT an `item` key is not unwrapped', async () => {
96
- // A metadata document may legitimately carry `type` and `name` of its own
97
- // (a view is `{name, type: "grid", ...}`). Absent `item`, it is the body.
98
- const view = { name: 'all_projects', type: 'grid', columns: ['name'] };
99
- const { c } = clientAnswering(view);
100
- expect(await c.get('view', 'all_projects')).toEqual(view);
101
- });
102
-
103
- it('A6: an `item` key WITHOUT the envelope identity is not unwrapped', async () => {
104
- // Presence-checked against the spec shape, not duck-guessed off `item`
105
- // alone — a body whose own schema has an `item` property stays whole.
106
- const body = { name: 'cart', item: { sku: 'x' } };
107
- const { c } = clientAnswering(body);
108
- expect(await c.get('object', 'cart')).toEqual(body);
109
- });
110
-
111
- it('A7: an array response passes through', async () => {
112
- const { c } = clientAnswering([{ name: 'a' }]);
113
- expect(await c.get('object', 'weird')).toEqual([{ name: 'a' }]);
114
- });
115
-
116
- it('A8: 404 still reads as null', async () => {
117
- const { c } = clientAnswering({}, { status: 404 });
118
- expect(await c.get('object', 'missing')).toBeNull();
119
- });
120
-
121
- it('A9: error responses still throw with status, code and message intact', async () => {
122
- const { c } = clientAnswering(
123
- { error: { code: 'RESOURCE_FORBIDDEN', message: 'Nope.' } },
124
- { status: 403, headers: { 'content-type': 'application/json' } },
125
- );
126
- await expect(c.get('object', 'secret')).rejects.toThrow('Nope.');
127
- const err = await c.get('object', 'secret').catch((e: MetadataError) => e);
128
- expect((err as MetadataError).status).toBe(403);
129
- expect((err as MetadataError).code).toBe('RESOURCE_FORBIDDEN');
130
- });
131
- });
132
-
133
- describe('objectui#4271 · the getDraft() envelope asymmetry is preserved', () => {
134
- it('A10: getDraft() returns the RAW envelope, as its docblock promises', async () => {
135
- // Load-bearing: ~11 call sites read `.item` off getDraft (StudioDesignSurface,
136
- // ResourceEditPage, PackageOwdOverviewPanel, ObjectHooksPanel,
137
- // PermissionMatrixEditor). Routing getDraft through the unwrapping get()
138
- // would silently empty every one of them.
139
- const draftEnvelope = { type: 'object', name: 'showcase_project', item: OBJECT_BODY };
140
- const { c, urls } = clientAnswering(draftEnvelope);
141
- const draft = await c.getDraft<Record<string, unknown>>('object', 'showcase_project');
142
- expect(draft).toEqual(draftEnvelope);
143
- expect((draft as any)?.item).toEqual(OBJECT_BODY);
144
- expect(urls[0]).toBe('http://localhost:3000/api/v1/meta/object/showcase_project?state=draft');
145
- });
146
-
147
- it('A11: get(state:draft) unwraps like any other get()', async () => {
148
- // The SHAPE follows the method's published contract, not the query param:
149
- // `get()` promises the body on every path it serves.
150
- const { c, urls } = clientAnswering(ENVELOPE);
151
- expect(await c.get('object', 'showcase_project', { state: 'draft' })).toEqual(OBJECT_BODY);
152
- expect(urls[0]).toBe('http://localhost:3000/api/v1/meta/object/showcase_project?state=draft');
153
- });
154
-
155
- it('A12: URL construction is untouched by the unwrap', async () => {
156
- const { c, urls } = clientAnswering(ENVELOPE);
157
- await c.get('object', 'has spaces', { packageId: 'app.crm' });
158
- expect(urls[0]).toBe('http://localhost:3000/api/v1/meta/object/has%20spaces?package=app.crm');
159
- const preview = new MetadataClient({
160
- baseUrl: 'http://localhost:3000',
161
- previewDrafts: true,
162
- fetch: mockFetch(async (url) => {
163
- urls.push(url);
164
- return jsonResponse(ENVELOPE);
165
- }),
166
- });
167
- // The overlay flag still rides, and the preview read unwraps too.
168
- expect(await preview.get('object', 'showcase_project')).toEqual(OBJECT_BODY);
169
- expect(urls[1]).toBe('http://localhost:3000/api/v1/meta/object/showcase_project?preview=draft');
170
- });
171
- });
@@ -1,226 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * `MetadataClient.save` must surface the runtime authoring gate's advisory
11
- * findings (objectui#4133; backend objectstack#7435, landed as `89d7b35a7`).
12
- *
13
- * The save SUCCEEDED — a 200, the row persisted, the version bumped. The gate
14
- * simply noticed something worth telling the author, and before this the
15
- * findings were parsed off the wire and dropped on the floor client-side. These
16
- * pins are the red-first evidence: with the emit removed from `save()`, the
17
- * "renders the findings" cases below fail because no event ever arrives.
18
- *
19
- * The measured example the card is built on: a `nightly_purge` flow whose only
20
- * defect is a `delete_record` node with `multi: true` and no filter yields
21
- * `errors = 0 / advisories = 1` — a 200 the author currently learns nothing from.
22
- */
23
-
24
- import { describe, it, expect, vi } from 'vitest';
25
- import {
26
- MetadataClient,
27
- readSaveAdvisories,
28
- type MetadataSaveAdvisoryEvent,
29
- type RuntimeAuthoringIssue,
30
- } from './metadata-client';
31
-
32
- /** The measured `nightly_purge` finding, in the spec's D3 shape. */
33
- const PURGE_ADVISORY: RuntimeAuthoringIssue = {
34
- severity: 'warning',
35
- rule: 'flow/delete-without-filter',
36
- where: 'flow "nightly_purge" · node "purge old rows"',
37
- path: 'flows[0].nodes[2].config.filters',
38
- message: 'this delete_record node sets multi: true with no filter, so it deletes every row',
39
- hint: 'add a filter, or set multi: false to delete a single record',
40
- };
41
-
42
- function saveResponse(body: unknown): Response {
43
- return new Response(JSON.stringify(body), {
44
- status: 200,
45
- headers: { 'content-type': 'application/json' },
46
- });
47
- }
48
-
49
- function clientWith(
50
- responseBody: unknown,
51
- onSaveAdvisory?: (ev: MetadataSaveAdvisoryEvent) => void,
52
- ) {
53
- return new MetadataClient({
54
- baseUrl: 'http://test.local',
55
- fetch: vi.fn(async () => saveResponse(responseBody)) as unknown as typeof fetch,
56
- ...(onSaveAdvisory ? { onSaveAdvisory } : {}),
57
- });
58
- }
59
-
60
- const CLEAN_BODY = { success: true, version: 'v2', seq: 4, state: 'active' };
61
-
62
- describe('MetadataClient.save — runtime authoring gate advisories (#4133)', () => {
63
- it('emits the findings a successful save returned', async () => {
64
- const events: MetadataSaveAdvisoryEvent[] = [];
65
- const client = clientWith(
66
- { ...CLEAN_BODY, advisories: [PURGE_ADVISORY] },
67
- (e) => events.push(e),
68
- );
69
-
70
- await client.save('flow', 'nightly_purge', { name: 'nightly_purge' });
71
-
72
- expect(events).toHaveLength(1);
73
- expect(events[0]).toEqual({
74
- type: 'flow',
75
- name: 'nightly_purge',
76
- mode: 'publish',
77
- advisories: [PURGE_ADVISORY],
78
- });
79
- });
80
-
81
- it('carries rule, message and hint through verbatim — they are server prose', async () => {
82
- const events: MetadataSaveAdvisoryEvent[] = [];
83
- const client = clientWith(
84
- { ...CLEAN_BODY, advisories: [PURGE_ADVISORY] },
85
- (e) => events.push(e),
86
- );
87
-
88
- await client.save('flow', 'nightly_purge', {});
89
-
90
- const [finding] = events[0]!.advisories;
91
- expect(finding!.rule).toBe('flow/delete-without-filter');
92
- expect(finding!.message).toBe(PURGE_ADVISORY.message);
93
- expect(finding!.hint).toBe(PURGE_ADVISORY.hint);
94
- // Never `error` on this channel — an error-severity finding is a 422.
95
- expect(finding!.severity).toBe('warning');
96
- });
97
-
98
- it('says nothing on a clean save — the server omits the key entirely', async () => {
99
- const events: MetadataSaveAdvisoryEvent[] = [];
100
- const client = clientWith(CLEAN_BODY, (e) => events.push(e));
101
-
102
- await client.save('object', 'account', { name: 'account' });
103
-
104
- expect(events).toEqual([]);
105
- });
106
-
107
- it('says nothing when the array is present but empty', async () => {
108
- const events: MetadataSaveAdvisoryEvent[] = [];
109
- const client = clientWith({ ...CLEAN_BODY, advisories: [] }, (e) => events.push(e));
110
-
111
- await client.save('object', 'account', {});
112
-
113
- expect(events).toEqual([]);
114
- });
115
-
116
- it('still returns the save response unchanged', async () => {
117
- const body = { ...CLEAN_BODY, advisories: [PURGE_ADVISORY] };
118
- const client = clientWith(body, () => {});
119
-
120
- const result = await client.save('flow', 'nightly_purge', {});
121
-
122
- expect(result).toEqual(body);
123
- });
124
-
125
- it('a throwing sink never fails a save the server already committed', async () => {
126
- const client = clientWith({ ...CLEAN_BODY, advisories: [PURGE_ADVISORY] }, () => {
127
- throw new Error('renderer exploded');
128
- });
129
-
130
- await expect(client.save('flow', 'nightly_purge', {})).resolves.toBeTruthy();
131
- });
132
-
133
- /**
134
- * The honest control for the coverage statement (#4133 requirement 3).
135
- *
136
- * A `mode: 'draft'` save is a PUT through the same save door, but the
137
- * framework's gate returns at its D1 early-return before running a single
138
- * rule — `runtime-authoring-gate.ts`:
139
- *
140
- * // D1 — drafts are never gated. Publishing one runs this same function.
141
- * if (args.state !== 'active') return null;
142
- *
143
- * so the server produces no findings and the response carries no key. The
144
- * client is not what suppresses this; there is nothing to suppress. Pinned
145
- * because Studio's designer saves as draft on EVERY edit, which is why the
146
- * affordance renders nothing on that flow today.
147
- */
148
- it('a draft save carries no findings — the gate never ran (D1)', async () => {
149
- const events: MetadataSaveAdvisoryEvent[] = [];
150
- const client = clientWith({ ...CLEAN_BODY, state: 'draft' }, (e) => events.push(e));
151
-
152
- await client.save('flow', 'nightly_purge', {}, { mode: 'draft' });
153
-
154
- expect(events).toEqual([]);
155
- });
156
-
157
- it('labels the mode when a draft save does somehow advise', async () => {
158
- // Not reachable through today's server (see D1 above), but the event's
159
- // `mode` must tell the truth about which door it came through rather than
160
- // hard-coding one, so the field is pinned on its own.
161
- const events: MetadataSaveAdvisoryEvent[] = [];
162
- const client = clientWith(
163
- { ...CLEAN_BODY, state: 'draft', advisories: [PURGE_ADVISORY] },
164
- (e) => events.push(e),
165
- );
166
-
167
- await client.save('flow', 'nightly_purge', {}, { mode: 'draft' });
168
-
169
- expect(events[0]!.mode).toBe('draft');
170
- });
171
-
172
- it('survives the withEnvironment clone — console clients are all env-scoped', async () => {
173
- const events: MetadataSaveAdvisoryEvent[] = [];
174
- const base = new MetadataClient({
175
- baseUrl: 'http://test.local',
176
- fetch: vi.fn(async () =>
177
- saveResponse({ ...CLEAN_BODY, advisories: [PURGE_ADVISORY] }),
178
- ) as unknown as typeof fetch,
179
- onSaveAdvisory: (e) => events.push(e),
180
- });
181
-
182
- await base.withEnvironment('env_1').save('flow', 'nightly_purge', {});
183
-
184
- expect(events).toHaveLength(1);
185
- });
186
-
187
- it('survives the withPreviewDrafts clone', async () => {
188
- const events: MetadataSaveAdvisoryEvent[] = [];
189
- const base = new MetadataClient({
190
- baseUrl: 'http://test.local',
191
- fetch: vi.fn(async () =>
192
- saveResponse({ ...CLEAN_BODY, advisories: [PURGE_ADVISORY] }),
193
- ) as unknown as typeof fetch,
194
- onSaveAdvisory: (e) => events.push(e),
195
- });
196
-
197
- await base.withPreviewDrafts(true).save('flow', 'nightly_purge', {});
198
-
199
- expect(events).toHaveLength(1);
200
- });
201
- });
202
-
203
- describe('readSaveAdvisories', () => {
204
- it('reads a well-formed array', () => {
205
- expect(readSaveAdvisories({ advisories: [PURGE_ADVISORY] })).toEqual([PURGE_ADVISORY]);
206
- });
207
-
208
- it('returns empty for a response with no advisories key', () => {
209
- expect(readSaveAdvisories(CLEAN_BODY)).toEqual([]);
210
- });
211
-
212
- it.each([
213
- ['null body', null],
214
- ['a string body', 'nope'],
215
- ['a non-array advisories', { advisories: { rule: 'x' } }],
216
- ])('returns empty for %s', (_label, body) => {
217
- expect(readSaveAdvisories(body)).toEqual([]);
218
- });
219
-
220
- it('drops half-shaped findings rather than rendering blanks at the author', () => {
221
- const out = readSaveAdvisories({
222
- advisories: [PURGE_ADVISORY, { rule: 'only-a-rule' }, null, 'string'],
223
- });
224
- expect(out).toEqual([PURGE_ADVISORY]);
225
- });
226
- });