@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
package/src/contracts.ts DELETED
@@ -1,115 +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
- * Contracts module integration for @objectstack/spec v3.0.0
11
- * Provides plugin contract validation and marketplace publishing utilities.
12
- */
13
-
14
- export interface PluginContract {
15
- /** Plugin name */
16
- name: string;
17
- /** Plugin version */
18
- version: string;
19
- /** Required peer dependencies */
20
- peerDependencies?: Record<string, string>;
21
- /** Exported component types */
22
- exports: PluginExport[];
23
- /** Required permissions */
24
- permissions?: string[];
25
- /** API surface contract */
26
- api?: PluginAPIContract;
27
- }
28
-
29
- export interface PluginExport {
30
- /** Export name */
31
- name: string;
32
- /** Export type */
33
- type: 'component' | 'hook' | 'utility' | 'provider';
34
- /** Description */
35
- description?: string;
36
- }
37
-
38
- export interface PluginAPIContract {
39
- /** Consumed data sources */
40
- dataSources?: string[];
41
- /** Required object schemas */
42
- requiredSchemas?: string[];
43
- /** Event subscriptions */
44
- events?: string[];
45
- }
46
-
47
- export interface ContractValidationResult {
48
- valid: boolean;
49
- errors: ContractValidationError[];
50
- warnings: string[];
51
- }
52
-
53
- export interface ContractValidationError {
54
- field: string;
55
- message: string;
56
- code: string;
57
- }
58
-
59
- /**
60
- * Validate a plugin contract against the ObjectStack spec.
61
- */
62
- export function validatePluginContract(contract: PluginContract): ContractValidationResult {
63
- const errors: ContractValidationError[] = [];
64
- const warnings: string[] = [];
65
-
66
- if (!contract.name || contract.name.trim().length === 0) {
67
- errors.push({ field: 'name', message: 'Plugin name is required', code: 'MISSING_NAME' });
68
- }
69
-
70
- if (!contract.version || !/^\d+\.\d+\.\d+/.test(contract.version)) {
71
- errors.push({ field: 'version', message: 'Valid semver version is required', code: 'INVALID_VERSION' });
72
- }
73
-
74
- if (!contract.exports || contract.exports.length === 0) {
75
- errors.push({ field: 'exports', message: 'At least one export is required', code: 'NO_EXPORTS' });
76
- }
77
-
78
- if (contract.exports) {
79
- const validTypes = ['component', 'hook', 'utility', 'provider'];
80
- for (const exp of contract.exports) {
81
- if (!exp.name) {
82
- errors.push({ field: 'exports.name', message: 'Export name is required', code: 'MISSING_EXPORT_NAME' });
83
- }
84
- if (!validTypes.includes(exp.type)) {
85
- errors.push({ field: 'exports.type', message: `Invalid export type: ${exp.type}`, code: 'INVALID_EXPORT_TYPE' });
86
- }
87
- }
88
- }
89
-
90
- if (!contract.permissions || contract.permissions.length === 0) {
91
- warnings.push('No permissions declared — plugin will have minimal access');
92
- }
93
-
94
- return { valid: errors.length === 0, errors, warnings };
95
- }
96
-
97
- /**
98
- * Generate a plugin contract manifest for marketplace publishing.
99
- */
100
- export function generateContractManifest(contract: PluginContract): Record<string, unknown> {
101
- return {
102
- $schema: 'https://objectui.org/schemas/plugin-contract-v1.json',
103
- name: contract.name,
104
- version: contract.version,
105
- peerDependencies: contract.peerDependencies ?? {},
106
- exports: contract.exports.map(exp => ({
107
- name: exp.name,
108
- type: exp.type,
109
- description: exp.description ?? '',
110
- })),
111
- permissions: contract.permissions ?? [],
112
- api: contract.api ?? {},
113
- generatedAt: new Date().toISOString(),
114
- };
115
- }
@@ -1,389 +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
- import { describe, it, expect, vi } from 'vitest';
10
- import { ObjectStackAdapter } from './index';
11
- import { MetadataClient } from './metadata-client';
12
-
13
- /**
14
- * `deleteView` removes every home the view has (#4479).
15
- *
16
- * A view has two possible homes — the pending per-item DRAFT
17
- * (`DELETE /meta/view/:name?state=draft`) and the PUBLISHED overlay
18
- * (`DELETE /meta/view/:name`) — and `deleteView` used to address only the
19
- * published one. The three cases, and what each used to do:
20
- *
21
- * | case | before #4479 |
22
- * |--------------------|-----------------------------------------------------|
23
- * | draft-only view | BUG — hits the published overlay, `reset:false`, the |
24
- * | | draft survives and the tab is back after reload |
25
- * | published-only | correct — `reset:true`, tab gone |
26
- * | published + draft | ACCIDENTALLY correct — the published row goes, so |
27
- * | | the tab goes; the orphan draft is left behind |
28
- *
29
- * The third row is why the mirror of #4139 is NOT mechanical.
30
- * `persistRuntimeMetadata` (app-shell) stages **every** runtime edit as a
31
- * draft, so "publish a view, then edit it" routinely produces a pair — and a
32
- * naive draft-FIRST-only mirror would have discarded just the draft there,
33
- * silently downgrading **Delete view** into **Discard draft**, which is a
34
- * different operation that already exists (`discardRuntimeDraft`).
35
- *
36
- * The ruling on #4479: delete BOTH homes, draft first. Draft first because a
37
- * failure between the two calls then leaves the PUBLISHED overlay intact and
38
- * the operation cleanly retryable — the reverse order strands a draft-only
39
- * view, which is this card's original bug shape.
40
- *
41
- * ## The wire these pins model
42
- *
43
- * Every receipt below is the framework's own, read verbatim from
44
- * `packages/metadata-protocol/src/protocol.ts` (`deleteMetaItem`) — the
45
- * no-row branch and the delete-ful branch. Both no-row answers are a **200**
46
- * carrying `reset:false`, NOT a 404:
47
- *
48
- * - no draft pending -> `{success:true, reset:false, message:"No pending draft for view/x."}`
49
- * - no published row -> `{success:true, reset:false, message:"No view 'x' found — nothing to delete."}`
50
- *
51
- * That measurement is what makes a blind two-call implementation legitimate
52
- * here: neither "missing home" answer is an error, so there is nothing for a
53
- * probe to protect against and the probe `updateView` needs (its read must
54
- * resolve the row it is about to merge onto) has no counterpart for a delete.
55
- *
56
- * Nothing here mocks `deleteView`. The adapter is real and drives a
57
- * wire-shaped `fetch`; the SDK stand-in on `ds.client.meta.deleteItem`
58
- * mirrors the real client's transport (same URL, same `unwrapResponse`) so
59
- * that the pins address the REQUEST, not the transport that issued it.
60
- */
61
-
62
- const VIEW = 'account.my_pipeline';
63
-
64
- /** Framework receipt: a draft row existed and was discarded. */
65
- const DRAFT_DISCARDED = {
66
- success: true,
67
- reset: true,
68
- seq: 41,
69
- message: `Draft discarded — view/${VIEW}. [seq=41]`,
70
- };
71
- /** Framework receipt: nothing pending. 200, not 404. */
72
- const NO_DRAFT = {
73
- success: true,
74
- reset: false,
75
- message: `No pending draft for view/${VIEW}.`,
76
- };
77
- /** Framework receipt: a runtime-only published row existed and is gone. */
78
- const PUBLISHED_DELETED = {
79
- success: true,
80
- reset: true,
81
- seq: 42,
82
- message: `Deleted view '${VIEW}' — it no longer exists. [seq=42]`,
83
- };
84
- /** Framework receipt: no published row. 200, not 404 — the answer #4479 quoted. */
85
- const NO_PUBLISHED = {
86
- success: true,
87
- reset: false,
88
- message: `No view '${VIEW}' found — nothing to delete.`,
89
- };
90
-
91
- interface Call {
92
- /** `'draft'` when the request carried `?state=draft`, else `'published'`. */
93
- home: 'draft' | 'published';
94
- method: string;
95
- url: string;
96
- }
97
-
98
- interface Harness {
99
- ds: any;
100
- /** Every DELETE against `/meta/view/...`, in issue order. */
101
- deletes: Call[];
102
- /** Just the home of each DELETE, in issue order — the ordering pin. */
103
- order: () => Array<'draft' | 'published'>;
104
- /** Keys passed to `metadataCache.invalidate`, in order. */
105
- invalidated: string[];
106
- baseUrl: string;
107
- fetchImpl: any;
108
- }
109
-
110
- /**
111
- * Adapter whose two delete addresses answer independently.
112
- *
113
- * @param opts.draft receipt (or Error/status) for `DELETE ...?state=draft`
114
- * @param opts.published receipt (or Error/status) for `DELETE ...` (active)
115
- */
116
- function makeDS(opts: {
117
- draft?: unknown | { failStatus: number };
118
- published?: unknown | { failStatus: number };
119
- } = {}): Harness {
120
- const deletes: Call[] = [];
121
- const invalidated: string[] = [];
122
- const baseUrl = 'http://test.local';
123
-
124
- const json = (body: unknown, status = 200) =>
125
- new Response(JSON.stringify(body), {
126
- status,
127
- headers: { 'Content-Type': 'application/json' },
128
- });
129
-
130
- const answer = (spec: unknown) => {
131
- if (spec && typeof spec === 'object' && 'failStatus' in (spec as any)) {
132
- const status = (spec as any).failStatus as number;
133
- return json({ error: { message: 'boom', code: 'server_error' } }, status);
134
- }
135
- return json(spec);
136
- };
137
-
138
- const fetchImpl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
139
- const url = String(input);
140
- const method = init?.method ?? 'GET';
141
- if (url.includes('/meta/view/')) {
142
- if (method === 'DELETE') {
143
- const home = url.includes('state=draft') ? 'draft' : 'published';
144
- deletes.push({ home, method, url });
145
- return answer(home === 'draft' ? opts.draft ?? NO_DRAFT : opts.published ?? NO_PUBLISHED);
146
- }
147
- return json({ error: 'not found' }, 404);
148
- }
149
- return json({ success: true, data: { capabilities: {}, routes: {} } });
150
- });
151
-
152
- const ds: any = new ObjectStackAdapter({ baseUrl, fetch: fetchImpl });
153
- ds.connected = true;
154
- ds.connectionState = 'connected';
155
-
156
- ds.metadataCache = {
157
- get: async (_key: string, loader: () => Promise<any>) => loader(),
158
- invalidate: (key: string) => {
159
- invalidated.push(key);
160
- },
161
- getCachedSync: () => undefined,
162
- getStats: () => ({}),
163
- };
164
-
165
- // SDK transport stand-in. NOT a stub of the behaviour under test: it issues
166
- // the same real request the shipped `client.meta.deleteItem` issues
167
- // (`${baseUrl}/api/v1/meta/:type/:name`, DELETE) and unwraps the
168
- // `{success, data}` envelope exactly as `unwrapResponse` does, so the
169
- // published half is observed at the WIRE whichever transport carries it.
170
- ds.client = {
171
- meta: {
172
- deleteItem: async (type: string, name: string) => {
173
- const res = await fetchImpl(
174
- `${baseUrl}/api/v1/meta/${encodeURIComponent(type)}/${encodeURIComponent(name)}`,
175
- { method: 'DELETE' },
176
- );
177
- const body: any = await res.json();
178
- if (!res.ok) {
179
- throw Object.assign(new Error(body?.error?.message ?? 'request failed'), {
180
- httpStatus: res.status,
181
- });
182
- }
183
- if (body && typeof body.success === 'boolean' && 'data' in body) return body.data;
184
- return body;
185
- },
186
- getItem: vi.fn(async () => {
187
- throw Object.assign(new Error('Metadata item not found'), { httpStatus: 404 });
188
- }),
189
- saveItem: vi.fn(async () => ({ success: true })),
190
- getItems: vi.fn(async () => ({ items: [] })),
191
- },
192
- };
193
-
194
- return {
195
- ds,
196
- deletes,
197
- order: () => deletes.map((c) => c.home),
198
- invalidated,
199
- baseUrl,
200
- fetchImpl,
201
- };
202
- }
203
-
204
- describe('deleteView removes every home the view has (#4479)', () => {
205
- // ── Row 1: draft-only — the reported bug ────────────────────────────────
206
- describe('draft-only view (the #4479 bug row)', () => {
207
- it('deletes the draft home, so the view is actually gone', async () => {
208
- const h = makeDS({ draft: DRAFT_DISCARDED, published: NO_PUBLISHED });
209
-
210
- const receipt = await h.ds.deleteView('account', VIEW);
211
-
212
- // The user-visible symptom first: before #4479 the only request issued
213
- // was the unqualified one, the server answered `reset:false` /
214
- // "nothing to delete", and this receipt came back `{deleted:false}`
215
- // with the draft untouched and the tab back after reload.
216
- expect(receipt.deleted).toBe(true);
217
- // ...because the draft home was never addressed.
218
- expect(h.order()).toContain('draft');
219
- });
220
-
221
- it('reports the draft home as the one that had the row', async () => {
222
- const h = makeDS({ draft: DRAFT_DISCARDED, published: NO_PUBLISHED });
223
-
224
- const receipt = await h.ds.deleteView('account', VIEW);
225
-
226
- expect(receipt.draft?.removed).toBe(true);
227
- expect(receipt.published?.removed).toBe(false);
228
- });
229
- });
230
-
231
- // ── Row 2: published-only — correct before, must stay correct ───────────
232
- describe('published-only view (correct before #4479)', () => {
233
- it('still deletes the published home and still reports deleted', async () => {
234
- const h = makeDS({ draft: NO_DRAFT, published: PUBLISHED_DELETED });
235
-
236
- const receipt = await h.ds.deleteView('account', VIEW);
237
-
238
- expect(h.order()).toContain('published');
239
- // Unchanged from before #4479 — this half was never broken.
240
- expect(receipt.deleted).toBe(true);
241
- });
242
-
243
- it('addresses both homes even when only one answers with a row', async () => {
244
- const h = makeDS({ draft: NO_DRAFT, published: PUBLISHED_DELETED });
245
-
246
- const receipt = await h.ds.deleteView('account', VIEW);
247
-
248
- expect(h.order()).toEqual(['draft', 'published']);
249
- expect(receipt.draft?.removed).toBe(false);
250
- expect(receipt.published?.removed).toBe(true);
251
- });
252
- });
253
-
254
- // ── Row 3: the pair — accidentally correct before; MUST NOT REGRESS ─────
255
- describe('published + pending draft pair (must-not-change + the anti-mirror guard)', () => {
256
- it('MUST NOT CHANGE: the published home is still deleted, so the tab still goes', async () => {
257
- const h = makeDS({ draft: DRAFT_DISCARDED, published: PUBLISHED_DELETED });
258
-
259
- const receipt = await h.ds.deleteView('account', VIEW);
260
-
261
- // This is the assertion a naive draft-first mirror of #4139 would have
262
- // broken: it would have discarded the draft and returned, leaving the
263
- // published row serving the view and the tab present after reload.
264
- expect(h.order()).toContain('published');
265
- expect(receipt.deleted).toBe(true);
266
- });
267
-
268
- it('also discards the orphan draft, so a republish cannot resurrect the view', async () => {
269
- const h = makeDS({ draft: DRAFT_DISCARDED, published: PUBLISHED_DELETED });
270
-
271
- const receipt = await h.ds.deleteView('account', VIEW);
272
-
273
- expect(h.order()).toEqual(['draft', 'published']);
274
- expect(receipt.draft?.removed).toBe(true);
275
- expect(receipt.published?.removed).toBe(true);
276
- });
277
-
278
- it('invalidates both view cache keys exactly once (#4363 rule, unchanged)', async () => {
279
- const h = makeDS({ draft: DRAFT_DISCARDED, published: PUBLISHED_DELETED });
280
-
281
- await h.ds.deleteView('account', VIEW);
282
-
283
- // Two homes deleted, still ONE ordered pair: the #4363 rule is per
284
- // WRITE METHOD, not per request.
285
- expect(h.invalidated).toEqual([`view:account:${VIEW}`, 'view-overrides:account']);
286
- });
287
- });
288
-
289
- // ── Row 4: neither home — the control ───────────────────────────────────
290
- it('MUST NOT CHANGE: a view with no home at all reports deleted:false', async () => {
291
- const h = makeDS({ draft: NO_DRAFT, published: NO_PUBLISHED });
292
-
293
- const receipt = await h.ds.deleteView('account', VIEW);
294
-
295
- // Both homes answer 200 `reset:false`. Nothing was removed and the
296
- // receipt has never claimed otherwise — that answer is preserved.
297
- expect(receipt.deleted).toBe(false);
298
- });
299
-
300
- // ── Ordering ────────────────────────────────────────────────────────────
301
- it('addresses the draft home strictly before the published one', async () => {
302
- const h = makeDS({ draft: DRAFT_DISCARDED, published: PUBLISHED_DELETED });
303
-
304
- await h.ds.deleteView('account', VIEW);
305
-
306
- expect(h.order()).toEqual(['draft', 'published']);
307
- expect(h.deletes[0].url).toContain('state=draft');
308
- expect(h.deletes[1].url).not.toContain('state=draft');
309
- });
310
-
311
- // ── One transport, one error contract ───────────────────────────────────
312
- it('sends both deletes to the same `/api/v1/meta/view/:name` route', async () => {
313
- const h = makeDS({ draft: DRAFT_DISCARDED, published: PUBLISHED_DELETED });
314
-
315
- await h.ds.deleteView('account', VIEW);
316
-
317
- const base = `${h.baseUrl}/api/v1/meta/view/${encodeURIComponent(VIEW)}`;
318
- expect(h.deletes[0].url).toBe(`${base}?state=draft`);
319
- expect(h.deletes[1].url).toBe(base);
320
- });
321
-
322
- // ── Failure paths — updateView's convention: throw, never degrade ───────
323
- describe('partial failure', () => {
324
- it('a failing draft delete throws and leaves the published home untouched', async () => {
325
- const h = makeDS({ draft: { failStatus: 500 }, published: PUBLISHED_DELETED });
326
-
327
- await expect(h.ds.deleteView('account', VIEW)).rejects.toThrow();
328
-
329
- // The whole reason the ruling put the draft FIRST: a mid-operation
330
- // failure must leave the published overlay serving the view, so the
331
- // user retries a delete rather than being left with the draft-only
332
- // shape that is this card's original bug.
333
- expect(h.order()).toEqual(['draft']);
334
- });
335
-
336
- it('a failing published delete throws rather than reporting deleted:true', async () => {
337
- const h = makeDS({ draft: DRAFT_DISCARDED, published: { failStatus: 500 } });
338
-
339
- await expect(h.ds.deleteView('account', VIEW)).rejects.toThrow();
340
-
341
- expect(h.order()).toEqual(['draft', 'published']);
342
- });
343
-
344
- it('the published-half failure carries the partial outcome, never rounds it to true', async () => {
345
- const h = makeDS({ draft: DRAFT_DISCARDED, published: { failStatus: 500 } });
346
-
347
- const err: any = await h.ds.deleteView('account', VIEW).catch((e: any) => e);
348
-
349
- // "draft gone, overlay left" is exactly what the old
350
- // `{ deleted: boolean }` could not express.
351
- expect(err.outcome?.deleted).toBe(false);
352
- expect(err.outcome?.draft?.removed).toBe(true);
353
- expect(err.outcome?.published?.removed).toBe(false);
354
- expect(err.cause).toBeDefined();
355
- });
356
-
357
- it('still invalidates the view cache keys when a half fails', async () => {
358
- const h = makeDS({ draft: DRAFT_DISCARDED, published: { failStatus: 500 } });
359
-
360
- await h.ds.deleteView('account', VIEW).catch(() => undefined);
361
-
362
- // The draft row IS gone, so a cache still holding it is stale for the
363
- // full TTL. #4363's asymmetry decides it: an unnecessary invalidation
364
- // costs one refetch, a missed one costs five minutes of stale overrides.
365
- expect(h.invalidated).toEqual([`view:account:${VIEW}`, 'view-overrides:account']);
366
- });
367
- });
368
-
369
- // ── discardRuntimeDraft stays a DIFFERENT operation ─────────────────────
370
- it('stays distinct from Discard draft, which touches only the draft home', async () => {
371
- // `discardRuntimeDraft` (app-shell `runtime-metadata-persistence.ts`) is
372
- // documented as "the published overlay is untouched" and is exactly this
373
- // one call. Driven here through the same primitive rather than imported,
374
- // because app-shell is downstream of this package — the point is that the
375
- // two operations issue DIFFERENT request sets against the same wire.
376
- const h = makeDS({ draft: DRAFT_DISCARDED, published: PUBLISHED_DELETED });
377
-
378
- await new MetadataClient({ baseUrl: h.baseUrl, fetch: h.fetchImpl }).reset('view', VIEW, {
379
- state: 'draft',
380
- });
381
- expect(h.order()).toEqual(['draft']);
382
-
383
- // Same wire, same view: Delete view addresses BOTH homes. If these two
384
- // ever issue the same request set, one of the operations has been lost.
385
- const fresh = makeDS({ draft: DRAFT_DISCARDED, published: PUBLISHED_DELETED });
386
- await fresh.ds.deleteView('account', VIEW);
387
- expect(fresh.order()).toEqual(['draft', 'published']);
388
- });
389
- });