@object-ui/data-objectstack 17.2.0 → 17.5.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.
- package/CHANGELOG.md +772 -0
- package/README.md +3 -11
- package/dist/index.cjs +1232 -699
- package/dist/index.d.cts +1367 -906
- package/dist/index.d.ts +1367 -906
- package/dist/index.js +1222 -696
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +5 -5
- package/src/apiAccessDenied.test.ts +233 -0
- package/src/appAccessProbe.test.ts +147 -0
- package/src/cloud-surface-retired-4152.pin.test.ts +171 -0
- package/src/deleteView.homes.test.ts +389 -0
- package/src/deleteViewContract.types.test.ts +246 -0
- package/src/filter-operator-ast-parity.test.ts +75 -11
- package/src/httpFailureLogging.test.ts +109 -0
- package/src/index.ts +967 -100
- package/src/listViewOverrides.test.ts +241 -0
- package/src/metadata-client.get-envelope.test.ts +171 -0
- package/src/metadata-client.saveAdvisories.test.ts +226 -0
- package/src/metadata-client.ts +208 -11
- package/src/onMutation.test.ts +29 -2
- package/src/onSaveAdvisory.test.ts +311 -0
- package/src/onWriteWarning.test.ts +101 -0
- package/src/queryDataset.test.ts +411 -0
- package/src/spec-symbol-batch6.test.ts +25 -2
- package/src/updateView.draft.test.ts +242 -0
- package/src/v3-compat.test.ts +10 -21
- package/src/viewCacheInvalidation.pin.test.ts +260 -0
- package/src/viewInvalidationSeam.pin.test.ts +162 -0
- package/src/cloud.ts +0 -109
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Adapter operator table → filter-AST parity (#2901, objectstack#3948).
|
|
10
|
+
* Adapter operator table → filter-AST parity (#2901, objectstack#3948, #3641).
|
|
11
11
|
*
|
|
12
12
|
* `FILTER_OPERATOR_ALIASES` is the last translation a filter passes through
|
|
13
13
|
* before it goes on the wire, and `normalizeFilterOperator` ends in `?? op` —
|
|
@@ -20,7 +20,31 @@
|
|
|
20
20
|
* `VIEW_FILTER_OPERATORS` — were missing, which is exactly how a stored
|
|
21
21
|
* "close_date before X" view came back unfiltered.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
23
|
+
* That failure description is historical, and one part of it has moved (measured
|
|
24
|
+
* for #3641, not assumed): on the published `@objectstack/driver-sql@17.0.0-rc.5`
|
|
25
|
+
* the array form is lowered by `parseFilterAST()` at the engine and protocol
|
|
26
|
+
* doors, and an array reaching the driver anyway is refused with a 400
|
|
27
|
+
* (`unsupportedFilterError`, objectstack#5158) rather than skipped. What the
|
|
28
|
+
* protocol door does with an array the AST gate REJECTS is not measurable from
|
|
29
|
+
* this repo, so whether a missing row costs an unfiltered read or a hard 400
|
|
30
|
+
* today is deliberately left open. Both are a broken query.
|
|
31
|
+
*
|
|
32
|
+
* ## What the coverage guarantee rests on (#3641)
|
|
33
|
+
*
|
|
34
|
+
* The coverage sweep below used to mirror production's `?? op` tail: resolve the
|
|
35
|
+
* operator through the table, fall back to the raw view spelling, then ask
|
|
36
|
+
* whether the result was a member of `VALID_AST_OPERATORS`. That reads as a
|
|
37
|
+
* coverage assertion and is not one. `VALID_AST_OPERATORS` is derived upstream
|
|
38
|
+
* from `AST_OPERATOR_MAP`, it grew until it spelled the canonical view operators
|
|
39
|
+
* verbatim — `before` and `after` included — and from that moment the fallback
|
|
40
|
+
* arm was always AST-valid. Emptying this entire table left the sweep green, so
|
|
41
|
+
* the one thing it existed to catch, a missing row, had become invisible to it.
|
|
42
|
+
*
|
|
43
|
+
* The sweep therefore asserts the row EXISTS, with no fallback: the header above
|
|
44
|
+
* says a missing row is an unfiltered query rather than a validation failure, so
|
|
45
|
+
* a missing row is what is asserted, directly. What the rows resolve TO is a
|
|
46
|
+
* separate question, kept as its own assertion below — and the pair that
|
|
47
|
+
* actually regressed is pinned by spelling, not by membership.
|
|
24
48
|
*/
|
|
25
49
|
import { describe, it, expect } from 'vitest';
|
|
26
50
|
import { VALID_AST_OPERATORS } from '@objectstack/spec/data';
|
|
@@ -30,6 +54,10 @@ import { FILTER_OPERATOR_ALIASES } from './index';
|
|
|
30
54
|
/**
|
|
31
55
|
* View operators this adapter is not the bridge for — the value-shape ones the
|
|
32
56
|
* view layer resolves to a null comparison before an operator is ever emitted.
|
|
57
|
+
*
|
|
58
|
+
* Every token here must still be a member of `VIEW_FILTER_OPERATORS` — the
|
|
59
|
+
* ratchet below enforces it. Subtracting a name the spec has retired excuses
|
|
60
|
+
* nothing and must be deleted rather than left as a dead subtraction (#3628).
|
|
33
61
|
*/
|
|
34
62
|
const NOT_THIS_ADAPTERS_JOB = new Set(['is_empty', 'is_not_empty']);
|
|
35
63
|
|
|
@@ -39,6 +67,33 @@ describe('FILTER_OPERATOR_ALIASES lands inside the spec AST vocabulary', () => {
|
|
|
39
67
|
expect(VALID_AST_OPERATORS.size).toBeGreaterThan(0);
|
|
40
68
|
});
|
|
41
69
|
|
|
70
|
+
// The exclusion ratchet (#3628). The coverage sweep further down subtracts a
|
|
71
|
+
// hand-written set from a spec-derived vocabulary, and that subtraction only
|
|
72
|
+
// excuses something while the spec still lists the subtracted tokens. Once
|
|
73
|
+
// upstream retires or renames one, the sweep stays green (it is still total
|
|
74
|
+
// over what remains) but the row becomes dead weight, and its comment goes on
|
|
75
|
+
// telling the next reader that the view layer resolves this one to a null
|
|
76
|
+
// comparison — about an operator no author can declare any more. That is the
|
|
77
|
+
// shape that rotted 37 of 82 deny-list entries in #3601 with nothing to report
|
|
78
|
+
// it: a hand-written list beside a spec-derived vocabulary and no assertion
|
|
79
|
+
// that its members still exist in that vocabulary.
|
|
80
|
+
//
|
|
81
|
+
// Collected rather than asserted per entry on purpose (same call as PR #3623):
|
|
82
|
+
// vocabulary retirements land as whole families, and failing on the first entry
|
|
83
|
+
// would hide the rest.
|
|
84
|
+
it('every NOT_THIS_ADAPTERS_JOB token is still in the spec view vocabulary', () => {
|
|
85
|
+
const vocabulary = new Set<string>(VIEW_FILTER_OPERATORS);
|
|
86
|
+
const retired = [...NOT_THIS_ADAPTERS_JOB].filter((op) => !vocabulary.has(op));
|
|
87
|
+
expect(
|
|
88
|
+
retired,
|
|
89
|
+
`VIEW_FILTER_OPERATORS no longer lists these NOT_THIS_ADAPTERS_JOB tokens: `
|
|
90
|
+
+ `${retired.join(', ')}. The spec has retired them, so subtracting them from `
|
|
91
|
+
+ 'the coverage sweep below excuses nothing — delete each from the set (with '
|
|
92
|
+
+ 'the comment claiming the view layer resolves it) rather than leaving a dead '
|
|
93
|
+
+ 'subtraction',
|
|
94
|
+
).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
42
97
|
it('every alias target is an operator the AST gate accepts', () => {
|
|
43
98
|
const bad = Object.entries(FILTER_OPERATOR_ALIASES)
|
|
44
99
|
.filter(([, target]) => !VALID_AST_OPERATORS.has(String(target).toLowerCase()))
|
|
@@ -50,17 +105,26 @@ describe('FILTER_OPERATOR_ALIASES lands inside the spec AST vocabulary', () => {
|
|
|
50
105
|
).toEqual([]);
|
|
51
106
|
});
|
|
52
107
|
|
|
53
|
-
it('
|
|
54
|
-
|
|
108
|
+
it('has a mapping row for every canonical view operator the spec defines', () => {
|
|
109
|
+
// Resolution mirrors `normalizeFilterOperator` — lowercased spelling first,
|
|
110
|
+
// then the operator as written — but stops short of its `?? op` tail. That
|
|
111
|
+
// tail is production behaviour and must stay there; reproducing it HERE is
|
|
112
|
+
// what cancelled this assertion (#3641), because the value it falls back to
|
|
113
|
+
// is the raw view spelling and the AST vocabulary now accepts all of those.
|
|
114
|
+
const hasRow = (op: string) =>
|
|
115
|
+
Object.prototype.hasOwnProperty.call(FILTER_OPERATOR_ALIASES, op.toLowerCase())
|
|
116
|
+
|| Object.prototype.hasOwnProperty.call(FILTER_OPERATOR_ALIASES, op);
|
|
117
|
+
|
|
118
|
+
const missing = VIEW_FILTER_OPERATORS
|
|
55
119
|
.filter((op) => !NOT_THIS_ADAPTERS_JOB.has(op))
|
|
56
|
-
.filter((op) =>
|
|
57
|
-
const target = FILTER_OPERATOR_ALIASES[op] ?? op;
|
|
58
|
-
return !VALID_AST_OPERATORS.has(String(target).toLowerCase());
|
|
59
|
-
});
|
|
120
|
+
.filter((op) => !hasRow(op));
|
|
60
121
|
expect(
|
|
61
|
-
|
|
62
|
-
'
|
|
63
|
-
+ '
|
|
122
|
+
missing,
|
|
123
|
+
'FILTER_OPERATOR_ALIASES has no row for these. An author can declare them on a '
|
|
124
|
+
+ 'ViewFilterRule and the spec validates them, so they reach the wire as the raw '
|
|
125
|
+
+ 'view spelling. Do not settle for "the AST gate happens to accept that" — the '
|
|
126
|
+
+ 'gate accepting a spelling is not the driver compiling it into a WHERE clause, '
|
|
127
|
+
+ 'and an unmapped operator is how this adapter shipped an unfiltered query',
|
|
64
128
|
).toEqual([]);
|
|
65
129
|
});
|
|
66
130
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* objectui#4042 half 2 — a request failure must SAY which request failed.
|
|
3
|
+
*
|
|
4
|
+
* `@objectstack/client` reports every non-2xx as
|
|
5
|
+
* `logger.error("HTTP request failed", undefined, { method, url, status, error })`.
|
|
6
|
+
* The console's logger used to forward that verbatim, so the identifying
|
|
7
|
+
* fields lived only in the third argument: anything that flattens a console
|
|
8
|
+
* record to text rendered them `[object Object]` / `Object`, and a screenful of
|
|
9
|
+
* failures told you nothing about which URL or which status. These tests pin
|
|
10
|
+
* that the message string itself now carries method + url + status (+ code).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
14
|
+
import { formatHttpFailureMessage, createQuietHttpLogger } from './index';
|
|
15
|
+
|
|
16
|
+
const META_401 = {
|
|
17
|
+
method: 'GET',
|
|
18
|
+
url: '/api/v1/meta/object',
|
|
19
|
+
status: 401,
|
|
20
|
+
error: { code: 'UNAUTHORIZED', message: 'Unauthorized' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('formatHttpFailureMessage', () => {
|
|
24
|
+
it('folds method, url and status into the message', () => {
|
|
25
|
+
expect(formatHttpFailureMessage('HTTP request failed', META_401)).toBe(
|
|
26
|
+
'HTTP request failed: GET /api/v1/meta/object -> 401 [UNAUTHORIZED]',
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('omits the code when the body carries none', () => {
|
|
31
|
+
expect(
|
|
32
|
+
formatHttpFailureMessage('HTTP request failed', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
url: '/api/v1/data/task',
|
|
35
|
+
status: 500,
|
|
36
|
+
}),
|
|
37
|
+
).toBe('HTTP request failed: POST /api/v1/data/task -> 500');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('reads the `statusCode` spelling too', () => {
|
|
41
|
+
expect(
|
|
42
|
+
formatHttpFailureMessage('HTTP request failed', { url: '/api/v1/meta/app', statusCode: 403 }),
|
|
43
|
+
).toBe('HTTP request failed: GET /api/v1/meta/app -> 403');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns null — not a husk — when meta identifies nothing', () => {
|
|
47
|
+
expect(formatHttpFailureMessage('HTTP request failed', undefined)).toBeNull();
|
|
48
|
+
expect(formatHttpFailureMessage('HTTP request failed', {})).toBeNull();
|
|
49
|
+
expect(formatHttpFailureMessage('HTTP request failed', { error: { message: 'boom' } })).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('never yields the un-diagnosable string the card reported', () => {
|
|
53
|
+
const line = formatHttpFailureMessage('HTTP request failed', META_401)!;
|
|
54
|
+
expect(line).not.toContain('[object Object]');
|
|
55
|
+
expect(line).toContain('/api/v1/meta/object');
|
|
56
|
+
expect(line).toContain('401');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('createQuietHttpLogger', () => {
|
|
61
|
+
afterEach(() => vi.restoreAllMocks());
|
|
62
|
+
|
|
63
|
+
it('logs a 401 as an error whose FIRST argument identifies the request', () => {
|
|
64
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
65
|
+
createQuietHttpLogger().error('HTTP request failed', undefined, META_401);
|
|
66
|
+
|
|
67
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
68
|
+
const [message, , meta] = spy.mock.calls[0];
|
|
69
|
+
expect(message).toBe('HTTP request failed: GET /api/v1/meta/object -> 401 [UNAUTHORIZED]');
|
|
70
|
+
// The structured bag is still handed over for DevTools to expand — the
|
|
71
|
+
// string is added alongside it, not instead of it.
|
|
72
|
+
expect(meta).toEqual(META_401);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('keeps demoting an expected 404, and identifies it in the demoted line too', () => {
|
|
76
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
77
|
+
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
78
|
+
|
|
79
|
+
createQuietHttpLogger().error('HTTP request failed', undefined, {
|
|
80
|
+
method: 'GET',
|
|
81
|
+
url: '/api/v1/data/sys_presence',
|
|
82
|
+
status: 404,
|
|
83
|
+
error: { code: 'OBJECT_NOT_FOUND' },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
87
|
+
expect(debugSpy).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(debugSpy.mock.calls[0][0]).toContain('/api/v1/data/sys_presence -> 404');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('does NOT silence a 401 — only 404-on-an-optional-collection is demoted', () => {
|
|
92
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
93
|
+
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
94
|
+
|
|
95
|
+
// Session expiry mid-use: the console gates doomed requests, it does not
|
|
96
|
+
// hide the ones that still fail.
|
|
97
|
+
createQuietHttpLogger().error('HTTP request failed', undefined, META_401);
|
|
98
|
+
|
|
99
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
100
|
+
expect(debugSpy).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('falls back to the bare message when there is nothing to identify', () => {
|
|
104
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
105
|
+
createQuietHttpLogger().error('Something broke', undefined, undefined);
|
|
106
|
+
|
|
107
|
+
expect(spy.mock.calls[0][0]).toBe('Something broke');
|
|
108
|
+
});
|
|
109
|
+
});
|