@agentshouse/mdmodel 0.1.0-cd90851746292 → 0.1.0-cdbd463e01dc2
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/API.md +90 -81
- package/README.md +6 -4
- package/dist/definition.d.ts +1 -1
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +13 -9
- package/dist/event.d.ts +78 -0
- package/dist/event.d.ts.map +1 -0
- package/dist/event.js +230 -0
- package/dist/facts.d.ts +15 -0
- package/dist/facts.d.ts.map +1 -0
- package/dist/facts.js +39 -0
- package/dist/index.d.ts +17 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/kinds.d.ts +19 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +161 -0
- package/dist/query/language.d.ts +223 -0
- package/dist/query/language.d.ts.map +1 -0
- package/dist/query/language.js +565 -0
- package/dist/query/match.d.ts +14 -0
- package/dist/query/match.d.ts.map +1 -0
- package/dist/query/match.js +121 -0
- package/dist/query/rows.d.ts +32 -0
- package/dist/query/rows.d.ts.map +1 -0
- package/dist/query/rows.js +161 -0
- package/dist/query/sql.d.ts +39 -0
- package/dist/query/sql.d.ts.map +1 -0
- package/dist/query/sql.js +279 -0
- package/dist/reserved.d.ts +5 -0
- package/dist/reserved.d.ts.map +1 -0
- package/dist/reserved.js +20 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts +4 -3
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +103 -36
- package/fixtures/README.md +17 -2
- package/fixtures/cases/link-into-a-reserved-name/context.json +1 -0
- package/fixtures/cases/link-into-a-reserved-name/expected.json +5 -0
- package/fixtures/cases/link-into-a-reserved-name/library/notes/a.md +6 -0
- package/fixtures/cases/link-into-a-reserved-name/schema.yaml +3 -0
- package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/context.json +1 -0
- package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/expected.json +5 -0
- package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/library/notes/b.md +5 -0
- package/fixtures/cases/member-on-an-untyped-document/context.json +1 -0
- package/fixtures/cases/member-on-an-untyped-document/expected.json +6 -0
- package/fixtures/cases/member-on-an-untyped-document/library/note.md +4 -0
- package/fixtures/cases/member-on-another-type/context.json +1 -0
- package/fixtures/cases/member-on-another-type/expected.json +6 -0
- package/fixtures/cases/member-on-another-type/library/note.md +4 -0
- package/fixtures/cases/member-on-another-type/schema.yaml +7 -0
- package/fixtures/cases/reference-into-a-reserved-name/context.json +1 -0
- package/fixtures/cases/reference-into-a-reserved-name/expected.json +6 -0
- package/fixtures/cases/reference-into-a-reserved-name/library/notes/a.md +4 -0
- package/fixtures/cases/reference-into-a-reserved-name/library/settings/brief.md +1 -0
- package/fixtures/cases/reference-into-a-reserved-name/schema.yaml +3 -0
- package/fixtures/cases/room-md-declares-a-type/context.json +1 -0
- package/fixtures/cases/room-md-declares-a-type/expected.json +6 -0
- package/fixtures/cases/room-md-declares-a-type/library/ROOM.md +5 -0
- package/fixtures/cases/typed-path-names-another-type/context.json +11 -0
- package/fixtures/cases/typed-path-names-another-type/expected.json +6 -0
- package/fixtures/cases/typed-path-names-another-type/library/settings/prefs/one.md +3 -0
- package/fixtures/cases/typed-path-names-another-type/schema.yaml +6 -0
- package/fixtures/cases/typed-path-names-its-type/context.json +11 -0
- package/fixtures/cases/typed-path-names-its-type/expected.json +3 -0
- package/fixtures/cases/typed-path-names-its-type/library/settings/prefs/deeper/two.md +1 -0
- package/fixtures/cases/typed-path-names-its-type/library/settings/prefs/one.md +4 -0
- package/fixtures/cases/typed-path-names-its-type/schema.yaml +6 -0
- package/fixtures/cases/typed-path-names-no-type/context.json +11 -0
- package/fixtures/cases/typed-path-names-no-type/expected.json +6 -0
- package/fixtures/cases/typed-path-names-no-type/library/settings/prefs/one.md +1 -0
- package/fixtures/cases/typed-path-names-no-type/schema.yaml +6 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/expected.json +6 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/library/rooms/team/tasks/plan.md +4 -0
- package/fixtures/cases/unnamed-typed-path-checks-its-fields/schema.yaml +5 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/expected.json +6 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/library/rooms/team/tasks/plan.md +6 -0
- package/fixtures/cases/unnamed-typed-path-names-a-type/schema.yaml +5 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/expected.json +3 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/library/rooms/team/plan.md +1 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/library/rooms/team/tasks/plan.md +5 -0
- package/fixtures/cases/unnamed-typed-path-reads-its-type/schema.yaml +5 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/context.json +11 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/expected.json +5 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/library/rooms/team/tasks/plan.md +5 -0
- package/fixtures/cases/unnamed-typed-path-without-a-body/schema.yaml +5 -0
- package/fixtures/queries.json +913 -0
- package/package.json +1 -1
- package/dist/condition.d.ts +0 -180
- package/dist/condition.d.ts.map +0 -1
- package/dist/condition.js +0 -633
package/dist/condition.js
DELETED
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
import { ARRAY_INDEX, escapePointer, fieldFacts, itemFacts, memberFacts, pointerFacts, pointerSegments, } from './definition.js';
|
|
2
|
-
import { roomPath, target } from './paths.js';
|
|
3
|
-
import { plain } from './yaml.js';
|
|
4
|
-
export const CONDITION_OPERATORS = [
|
|
5
|
-
'eq',
|
|
6
|
-
'in',
|
|
7
|
-
'exists',
|
|
8
|
-
'contains',
|
|
9
|
-
'gt',
|
|
10
|
-
'gte',
|
|
11
|
-
'lt',
|
|
12
|
-
'lte',
|
|
13
|
-
'prefix',
|
|
14
|
-
];
|
|
15
|
-
export const TEXT_FIELD = '/text';
|
|
16
|
-
export const WRITE_DOORS = ['browser', 'agent', 'client', 'function'];
|
|
17
|
-
const PROSE = { kind: 'prose' };
|
|
18
|
-
const NULL = { kind: 'null' };
|
|
19
|
-
const UNREADABLE = { kind: 'unreadable' };
|
|
20
|
-
const UNREAD = { present: undefined, values: [undefined], items: [undefined] };
|
|
21
|
-
const NO_ITEM = { present: false, values: [], items: [] };
|
|
22
|
-
const DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
23
|
-
const HEADER = /^\/headers\/[!#$%&'*+.^_`|~0-9a-z-]+$/u;
|
|
24
|
-
const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/u;
|
|
25
|
-
export function factKind(facts) {
|
|
26
|
-
if (facts.reference !== undefined)
|
|
27
|
-
return 'reference';
|
|
28
|
-
if (facts.values !== undefined)
|
|
29
|
-
return 'enum';
|
|
30
|
-
if (facts.type === 'number' || facts.type === 'integer')
|
|
31
|
-
return 'number';
|
|
32
|
-
if (facts.type === 'boolean')
|
|
33
|
-
return 'boolean';
|
|
34
|
-
if (facts.type !== 'string')
|
|
35
|
-
return null;
|
|
36
|
-
if (facts.format === 'date')
|
|
37
|
-
return 'date';
|
|
38
|
-
if (facts.format === 'date-time')
|
|
39
|
-
return 'time';
|
|
40
|
-
return 'string';
|
|
41
|
-
}
|
|
42
|
-
function fits(kind, facts, value) {
|
|
43
|
-
if (!scalar(value))
|
|
44
|
-
return false;
|
|
45
|
-
if (kind === 'enum')
|
|
46
|
-
return (facts.values ?? []).includes(value);
|
|
47
|
-
if (kind === 'number')
|
|
48
|
-
return typeof value === 'number';
|
|
49
|
-
if (kind === 'boolean')
|
|
50
|
-
return typeof value === 'boolean';
|
|
51
|
-
if (typeof value !== 'string')
|
|
52
|
-
return false;
|
|
53
|
-
if (kind === 'date')
|
|
54
|
-
return dated(value);
|
|
55
|
-
if (kind === 'time')
|
|
56
|
-
return instant(value) !== undefined;
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
function factOf(value, declared, place) {
|
|
60
|
-
if (value === null)
|
|
61
|
-
return NULL;
|
|
62
|
-
if (Array.isArray(value)) {
|
|
63
|
-
if (declared?.type !== undefined && declared.type !== 'array')
|
|
64
|
-
return UNREADABLE;
|
|
65
|
-
return {
|
|
66
|
-
kind: 'list',
|
|
67
|
-
items: value.map((item, index) => factOf(item, declared === undefined ? undefined : itemFacts(declared, index), place)),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
if (plain(value)) {
|
|
71
|
-
if (declared?.type !== undefined && declared.type !== 'object')
|
|
72
|
-
return UNREADABLE;
|
|
73
|
-
const fields = Object.create(null);
|
|
74
|
-
for (const [key, held] of Object.entries(value))
|
|
75
|
-
fields[key] = factOf(held, declared === undefined ? undefined : memberFacts(declared, key), place);
|
|
76
|
-
return { kind: 'object', fields };
|
|
77
|
-
}
|
|
78
|
-
if (declared?.type === 'array' || declared?.type === 'object')
|
|
79
|
-
return UNREADABLE;
|
|
80
|
-
const kind = declared === undefined || declared.prose ? null : factKind(declared);
|
|
81
|
-
if (kind === null)
|
|
82
|
-
return PROSE;
|
|
83
|
-
if (!fits(kind, declared, value))
|
|
84
|
-
return UNREADABLE;
|
|
85
|
-
if (kind !== 'reference')
|
|
86
|
-
return { kind, value };
|
|
87
|
-
const reached = target(place.path, value) ?? roomPath(value);
|
|
88
|
-
const resolved = reached === null ? undefined : place.resolve(reached);
|
|
89
|
-
if (resolved === undefined)
|
|
90
|
-
return UNREADABLE;
|
|
91
|
-
return resolved.member === undefined
|
|
92
|
-
? { kind, value: resolved.id }
|
|
93
|
-
: { kind, value: resolved.id, member: resolved.member };
|
|
94
|
-
}
|
|
95
|
-
export function facts(document, at, model, resolve) {
|
|
96
|
-
const type = document.type ?? null;
|
|
97
|
-
const definition = type !== null && Object.hasOwn(model.types, type) ? model.types[type] : null;
|
|
98
|
-
const fields = Object.create(null);
|
|
99
|
-
const place = { path: at.path, resolve };
|
|
100
|
-
for (const [name, value] of Object.entries(document.fields)) {
|
|
101
|
-
const declared = definition === null ? undefined : fieldFacts(definition, name);
|
|
102
|
-
if (declared !== undefined)
|
|
103
|
-
fields[name] = factOf(value, declared, place);
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
id: at.id,
|
|
107
|
-
path: `/${at.path}`,
|
|
108
|
-
type,
|
|
109
|
-
fields,
|
|
110
|
-
sections: Object.keys(document.sections).sort(),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function rootOf(facts, prefix) {
|
|
114
|
-
return { pointer: prefix, fact: { kind: 'object', fields: facts.fields } };
|
|
115
|
-
}
|
|
116
|
-
export function prosePaths(facts) {
|
|
117
|
-
const paths = facts.sections.map((heading) => `/sections/${heading}`);
|
|
118
|
-
const visit = ({ pointer, fact }) => {
|
|
119
|
-
if (fact.kind === 'prose')
|
|
120
|
-
paths.push(pointer);
|
|
121
|
-
if (fact.kind === 'list')
|
|
122
|
-
fact.items.forEach((item, index) => visit({ pointer: `${pointer}/${index}`, fact: item }));
|
|
123
|
-
if (fact.kind === 'object')
|
|
124
|
-
for (const [key, held] of Object.entries(fact.fields))
|
|
125
|
-
visit({ pointer: `${pointer}/${escapePointer(key)}`, fact: held });
|
|
126
|
-
};
|
|
127
|
-
visit(rootOf(facts, '/fields'));
|
|
128
|
-
return paths.sort();
|
|
129
|
-
}
|
|
130
|
-
function walked(start, pointer) {
|
|
131
|
-
let reached = [start];
|
|
132
|
-
let several = false;
|
|
133
|
-
let missing = false;
|
|
134
|
-
const each = (from, next) => from.flatMap((one) => {
|
|
135
|
-
const found = next(one);
|
|
136
|
-
if (found === null)
|
|
137
|
-
missing = true;
|
|
138
|
-
return found ?? [];
|
|
139
|
-
});
|
|
140
|
-
for (const token of pointerSegments(pointer)) {
|
|
141
|
-
const step = ({ pointer: at, fact }) => {
|
|
142
|
-
if (fact.kind === 'unreadable')
|
|
143
|
-
return [{ pointer: at, fact }];
|
|
144
|
-
if (fact.kind === 'object')
|
|
145
|
-
return Object.hasOwn(fact.fields, token)
|
|
146
|
-
? [{ pointer: `${at}/${escapePointer(token)}`, fact: fact.fields[token] }]
|
|
147
|
-
: null;
|
|
148
|
-
if (fact.kind !== 'list')
|
|
149
|
-
return null;
|
|
150
|
-
if (ARRAY_INDEX.test(token)) {
|
|
151
|
-
const item = fact.items[Number(token)];
|
|
152
|
-
return item === undefined ? null : [{ pointer: `${at}/${token}`, fact: item }];
|
|
153
|
-
}
|
|
154
|
-
several = true;
|
|
155
|
-
return each(fact.items.map((item, index) => ({ pointer: `${at}/${index}`, fact: item })), step);
|
|
156
|
-
};
|
|
157
|
-
reached = each(reached, step);
|
|
158
|
-
}
|
|
159
|
-
return { reached, several, missing };
|
|
160
|
-
}
|
|
161
|
-
function expand(fact) {
|
|
162
|
-
return fact.kind === 'list' ? fact.items.flatMap(expand) : [fact];
|
|
163
|
-
}
|
|
164
|
-
function fieldHeld(fact) {
|
|
165
|
-
if (fact.kind === 'unreadable')
|
|
166
|
-
return undefined;
|
|
167
|
-
if (fact.kind === 'null')
|
|
168
|
-
return { kind: 'null' };
|
|
169
|
-
return 'value' in fact ? { kind: fact.kind, value: fact.value } : { kind: 'prose' };
|
|
170
|
-
}
|
|
171
|
-
function memberHeld(fact) {
|
|
172
|
-
if (fact.kind === 'unreadable')
|
|
173
|
-
return undefined;
|
|
174
|
-
return fact.kind === 'reference' && fact.member !== undefined
|
|
175
|
-
? { kind: 'member', value: fact.member }
|
|
176
|
-
: null;
|
|
177
|
-
}
|
|
178
|
-
function single(held) {
|
|
179
|
-
return { present: true, values: [held], items: null };
|
|
180
|
-
}
|
|
181
|
-
function documentRead({ reached, several, missing }, held) {
|
|
182
|
-
if (reached.length === 0)
|
|
183
|
-
return missing ? undefined : NO_ITEM;
|
|
184
|
-
const facts = reached.map(({ fact }) => fact);
|
|
185
|
-
const kept = (all) => all.map(held).filter((value) => value !== null);
|
|
186
|
-
const unknown = missing ? [undefined] : [];
|
|
187
|
-
const values = kept(facts.filter((fact) => fact.kind !== 'list'));
|
|
188
|
-
const listed = several || facts.some((fact) => fact.kind === 'list');
|
|
189
|
-
const present = facts.some((fact) => fact.kind === 'list') || values.some(Boolean);
|
|
190
|
-
return {
|
|
191
|
-
present: present ? true : values.length > 0 ? undefined : false,
|
|
192
|
-
values: [...values, ...unknown],
|
|
193
|
-
items: listed ? [...kept(facts.flatMap(expand)), ...unknown] : null,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
function readDocument(facts, path) {
|
|
197
|
-
const own = tested(DOCUMENT_TESTS, path);
|
|
198
|
-
if (own !== undefined)
|
|
199
|
-
return own.read(facts, path);
|
|
200
|
-
if (path.startsWith('/sections/'))
|
|
201
|
-
return facts.sections.includes(path.slice('/sections/'.length))
|
|
202
|
-
? single({ kind: 'prose' })
|
|
203
|
-
: undefined;
|
|
204
|
-
if (path.startsWith('/fields/'))
|
|
205
|
-
return documentRead(walked(rootOf(facts, '/fields'), path.slice('/fields'.length)), fieldHeld);
|
|
206
|
-
if (path.startsWith('/members/'))
|
|
207
|
-
return documentRead(walked(rootOf(facts, '/members'), path.slice('/members'.length)), memberHeld);
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
export function datePointers(fields) {
|
|
211
|
-
return Object.entries(fields)
|
|
212
|
-
.filter(([, facts]) => facts.type === 'string' && facts.format === 'date')
|
|
213
|
-
.map(([name]) => `/fields/${escapePointer(name)}`)
|
|
214
|
-
.sort();
|
|
215
|
-
}
|
|
216
|
-
export function valuedChange(definition, field) {
|
|
217
|
-
if (!field.startsWith('/fields/'))
|
|
218
|
-
return false;
|
|
219
|
-
const reached = pointerFacts(definition, field.slice('/fields'.length));
|
|
220
|
-
return (reached !== undefined &&
|
|
221
|
-
!reached.through &&
|
|
222
|
-
reached.facts.type !== 'array' &&
|
|
223
|
-
!reached.facts.prose &&
|
|
224
|
-
factKind(reached.facts) !== null);
|
|
225
|
-
}
|
|
226
|
-
export function changeFields(definition) {
|
|
227
|
-
return [
|
|
228
|
-
...Object.keys(definition.fields).map((name) => `/fields/${escapePointer(name)}`),
|
|
229
|
-
...Object.keys(definition.sections).map((heading) => `/sections/${heading}`),
|
|
230
|
-
];
|
|
231
|
-
}
|
|
232
|
-
export function memberPointers(definition, memberType) {
|
|
233
|
-
return Object.entries(definition.fields)
|
|
234
|
-
.filter(([, facts]) => (facts.type === 'array' ? facts.items : facts)?.reference?.includes(memberType) === true)
|
|
235
|
-
.map(([name]) => `/members/${escapePointer(name)}`);
|
|
236
|
-
}
|
|
237
|
-
function kindOperators(kind) {
|
|
238
|
-
switch (kind) {
|
|
239
|
-
case 'string':
|
|
240
|
-
return ['eq', 'in', 'prefix', 'exists'];
|
|
241
|
-
case 'boolean':
|
|
242
|
-
return ['eq', 'exists'];
|
|
243
|
-
case 'number':
|
|
244
|
-
case 'date':
|
|
245
|
-
case 'time':
|
|
246
|
-
return ['eq', 'in', 'gt', 'gte', 'lt', 'lte', 'exists'];
|
|
247
|
-
default:
|
|
248
|
-
return ['eq', 'in', 'exists'];
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
function pointerOperators({ facts, through }) {
|
|
252
|
-
const scalar = facts.type === 'array' ? facts.items : facts;
|
|
253
|
-
const kind = scalar === undefined || scalar.prose ? null : factKind(scalar);
|
|
254
|
-
if (kind === null)
|
|
255
|
-
return ['exists'];
|
|
256
|
-
if (facts.type === 'array')
|
|
257
|
-
return ['contains', 'exists'];
|
|
258
|
-
const own = kindOperators(kind);
|
|
259
|
-
return through ? [...own.filter((operator) => operator !== 'exists'), 'contains', 'exists'] : own;
|
|
260
|
-
}
|
|
261
|
-
function typeOperators(definition, path, memberType) {
|
|
262
|
-
if (path.startsWith('/sections/'))
|
|
263
|
-
return Object.hasOwn(definition.sections, path.slice('/sections/'.length)) ? ['exists'] : [];
|
|
264
|
-
const pointer = path.startsWith('/fields/')
|
|
265
|
-
? path.slice('/fields'.length)
|
|
266
|
-
: memberType !== null && memberPointers(definition, memberType).includes(path)
|
|
267
|
-
? path.slice('/members'.length)
|
|
268
|
-
: undefined;
|
|
269
|
-
const reached = pointer === undefined ? undefined : pointerFacts(definition, pointer);
|
|
270
|
-
return reached === undefined ? [] : pointerOperators(reached);
|
|
271
|
-
}
|
|
272
|
-
function envelope(path, kind, value) {
|
|
273
|
-
return {
|
|
274
|
-
path,
|
|
275
|
-
names: (named) => named === path,
|
|
276
|
-
kind,
|
|
277
|
-
operators: kindOperators(kind),
|
|
278
|
-
read: (facts) => {
|
|
279
|
-
const held = value(facts);
|
|
280
|
-
return held === null || held === undefined ? undefined : single({ kind, value: held });
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
function tested(tests, path) {
|
|
285
|
-
return tests.find(({ names }) => names(path));
|
|
286
|
-
}
|
|
287
|
-
function reader(tests, facts) {
|
|
288
|
-
return (path) => tested(tests, path)?.read(facts, path);
|
|
289
|
-
}
|
|
290
|
-
const DOCUMENT_TESTS = [
|
|
291
|
-
{
|
|
292
|
-
path: '/path',
|
|
293
|
-
names: (named) => named === '/path',
|
|
294
|
-
kind: 'string',
|
|
295
|
-
operators: ['eq', 'in', 'prefix'],
|
|
296
|
-
read: (facts) => single({ kind: 'string', value: facts.path }),
|
|
297
|
-
},
|
|
298
|
-
envelope('/type', 'enum', (facts) => facts.type),
|
|
299
|
-
];
|
|
300
|
-
const ACTOR_TESTS = [
|
|
301
|
-
envelope('/user', 'enum', (actor) => actor.user),
|
|
302
|
-
envelope('/site', 'enum', (actor) => actor.site),
|
|
303
|
-
envelope('/app', 'enum', (actor) => actor.app),
|
|
304
|
-
envelope('/door', 'enum', (actor) => actor.door),
|
|
305
|
-
];
|
|
306
|
-
const ITEM_TESTS = [
|
|
307
|
-
envelope('/delivery', 'enum', (item) => item.delivery),
|
|
308
|
-
envelope('/source', 'enum', (item) => item.source),
|
|
309
|
-
envelope('/container', 'string', (item) => item.container),
|
|
310
|
-
envelope('/author', 'string', (item) => item.author),
|
|
311
|
-
{
|
|
312
|
-
path: '/headers/<lowercase name>',
|
|
313
|
-
names: (named) => HEADER.test(named),
|
|
314
|
-
kind: 'string',
|
|
315
|
-
operators: kindOperators('string'),
|
|
316
|
-
read: (item, path) => {
|
|
317
|
-
const name = path.slice('/headers/'.length);
|
|
318
|
-
return Object.hasOwn(item.headers, name)
|
|
319
|
-
? single({ kind: 'string', value: item.headers[name] })
|
|
320
|
-
: undefined;
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
path: '/json/<JSON Pointer>',
|
|
325
|
-
names: (named) => named.startsWith('/json/'),
|
|
326
|
-
kind: null,
|
|
327
|
-
operators: CONDITION_OPERATORS,
|
|
328
|
-
read: (item, path) => item.json === null ? UNREAD : jsonRead(pointed(item.json.value, path.slice('/json'.length))),
|
|
329
|
-
},
|
|
330
|
-
];
|
|
331
|
-
const MESSAGE_TESTS = [
|
|
332
|
-
envelope('/sender', 'enum', (message) => message.sender),
|
|
333
|
-
envelope('/routine', 'string', (message) => message.routine),
|
|
334
|
-
envelope('/path', 'string', (message) => message.path),
|
|
335
|
-
];
|
|
336
|
-
export const DOCUMENT_PATHS = DOCUMENT_TESTS.map(({ path }) => path);
|
|
337
|
-
export const ACTOR_PATHS = ACTOR_TESTS.map(({ path }) => path);
|
|
338
|
-
export const ITEM_PATHS = ITEM_TESTS.map(({ path }) => path);
|
|
339
|
-
export const MESSAGE_PATHS = MESSAGE_TESTS.map(({ path }) => path);
|
|
340
|
-
export function testOperators(definitions, path, memberType) {
|
|
341
|
-
const own = tested(DOCUMENT_TESTS, path);
|
|
342
|
-
if (own !== undefined)
|
|
343
|
-
return own.operators;
|
|
344
|
-
return [
|
|
345
|
-
...new Set(definitions.flatMap((definition) => typeOperators(definition, path, memberType))),
|
|
346
|
-
];
|
|
347
|
-
}
|
|
348
|
-
export function actorOperators(path) {
|
|
349
|
-
return tested(ACTOR_TESTS, path)?.operators ?? [];
|
|
350
|
-
}
|
|
351
|
-
export function itemOperators(path) {
|
|
352
|
-
return tested(ITEM_TESTS, path)?.operators ?? [];
|
|
353
|
-
}
|
|
354
|
-
export function messageOperators(path) {
|
|
355
|
-
return tested(MESSAGE_TESTS, path)?.operators ?? [];
|
|
356
|
-
}
|
|
357
|
-
export function itemKind(path) {
|
|
358
|
-
return tested(ITEM_TESTS, path)?.kind ?? null;
|
|
359
|
-
}
|
|
360
|
-
export function dated(value) {
|
|
361
|
-
return (typeof value === 'string' && DATE.test(value) && instant(`${value}T00:00:00Z`) !== undefined);
|
|
362
|
-
}
|
|
363
|
-
export function instant(value) {
|
|
364
|
-
if (typeof value !== 'string')
|
|
365
|
-
return undefined;
|
|
366
|
-
const parts = DATE_TIME.exec(value);
|
|
367
|
-
if (parts === null)
|
|
368
|
-
return undefined;
|
|
369
|
-
const [year, month, day, hour, minute, second, offsetHours, offsetMinutes] = [
|
|
370
|
-
1, 2, 3, 4, 5, 6, 9, 10,
|
|
371
|
-
].map((at) => Number(parts[at] ?? 0));
|
|
372
|
-
const at = new Date(0);
|
|
373
|
-
at.setUTCFullYear(year, month - 1, day);
|
|
374
|
-
at.setUTCHours(hour, minute, second);
|
|
375
|
-
if (at.getUTCFullYear() !== year ||
|
|
376
|
-
at.getUTCMonth() !== month - 1 ||
|
|
377
|
-
at.getUTCDate() !== day ||
|
|
378
|
-
at.getUTCHours() !== hour ||
|
|
379
|
-
at.getUTCMinutes() !== minute ||
|
|
380
|
-
offsetHours > 23 ||
|
|
381
|
-
offsetMinutes > 59)
|
|
382
|
-
return undefined;
|
|
383
|
-
const offset = (parts[8] === '-' ? -1 : 1) * (offsetHours * 60 + offsetMinutes);
|
|
384
|
-
const fraction = (parts[7] ?? '').padEnd(9, '0').slice(0, 9);
|
|
385
|
-
return (BigInt(at.getTime()) - BigInt(offset) * 60000n) * 1000000n + BigInt(fraction);
|
|
386
|
-
}
|
|
387
|
-
export function pointed(value, pointer) {
|
|
388
|
-
let reached = value;
|
|
389
|
-
for (const token of pointerSegments(pointer)) {
|
|
390
|
-
if (Array.isArray(reached)) {
|
|
391
|
-
if (!ARRAY_INDEX.test(token))
|
|
392
|
-
return undefined;
|
|
393
|
-
reached = reached[Number(token)];
|
|
394
|
-
}
|
|
395
|
-
else if (plain(reached) && Object.hasOwn(reached, token)) {
|
|
396
|
-
reached = reached[token];
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
return undefined;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return reached;
|
|
403
|
-
}
|
|
404
|
-
function scalar(value) {
|
|
405
|
-
return (typeof value === 'string' ||
|
|
406
|
-
typeof value === 'boolean' ||
|
|
407
|
-
(typeof value === 'number' && Number.isFinite(value)));
|
|
408
|
-
}
|
|
409
|
-
function jsonHeld(value) {
|
|
410
|
-
if (typeof value === 'string')
|
|
411
|
-
return {
|
|
412
|
-
kind: dated(value) ? 'date' : instant(value) === undefined ? 'string' : 'time',
|
|
413
|
-
value,
|
|
414
|
-
};
|
|
415
|
-
if (typeof value === 'boolean')
|
|
416
|
-
return { kind: 'boolean', value };
|
|
417
|
-
return { kind: 'number', value };
|
|
418
|
-
}
|
|
419
|
-
function jsonRead(value) {
|
|
420
|
-
if (value === undefined)
|
|
421
|
-
return undefined;
|
|
422
|
-
if (scalar(value))
|
|
423
|
-
return single(jsonHeld(value));
|
|
424
|
-
if (!Array.isArray(value))
|
|
425
|
-
return single({ kind: 'prose' });
|
|
426
|
-
return { present: true, values: [], items: value.filter(scalar).map(jsonHeld), loose: true };
|
|
427
|
-
}
|
|
428
|
-
function equal(held, value) {
|
|
429
|
-
if (!('value' in held))
|
|
430
|
-
return undefined;
|
|
431
|
-
if (typeof value !== typeof held.value)
|
|
432
|
-
return undefined;
|
|
433
|
-
if (held.kind !== 'time')
|
|
434
|
-
return value === held.value;
|
|
435
|
-
const left = instant(held.value);
|
|
436
|
-
const right = instant(value);
|
|
437
|
-
return left === undefined || right === undefined ? undefined : left === right;
|
|
438
|
-
}
|
|
439
|
-
function sign(left, right) {
|
|
440
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
441
|
-
}
|
|
442
|
-
function ordered(held, value, compare) {
|
|
443
|
-
if (!('value' in held))
|
|
444
|
-
return undefined;
|
|
445
|
-
if (held.kind === 'number') {
|
|
446
|
-
if (typeof value !== 'number' || typeof held.value !== 'number')
|
|
447
|
-
return undefined;
|
|
448
|
-
return compare(Math.sign(held.value - value));
|
|
449
|
-
}
|
|
450
|
-
if (held.kind === 'date') {
|
|
451
|
-
if (!dated(value) || !dated(held.value))
|
|
452
|
-
return undefined;
|
|
453
|
-
return compare(sign(held.value, value));
|
|
454
|
-
}
|
|
455
|
-
if (held.kind !== 'time')
|
|
456
|
-
return undefined;
|
|
457
|
-
const at = instant(held.value);
|
|
458
|
-
const wanted = instant(value);
|
|
459
|
-
if (at === undefined || wanted === undefined)
|
|
460
|
-
return undefined;
|
|
461
|
-
return compare(sign(at, wanted));
|
|
462
|
-
}
|
|
463
|
-
function anyOf(truths) {
|
|
464
|
-
if (truths.some((truth) => truth === true))
|
|
465
|
-
return true;
|
|
466
|
-
return truths.some((truth) => truth === undefined) ? undefined : false;
|
|
467
|
-
}
|
|
468
|
-
function allOf(truths) {
|
|
469
|
-
if (truths.some((truth) => truth === false))
|
|
470
|
-
return false;
|
|
471
|
-
return truths.some((truth) => truth === undefined) ? undefined : true;
|
|
472
|
-
}
|
|
473
|
-
function compared(test, held) {
|
|
474
|
-
if ('eq' in test)
|
|
475
|
-
return equal(held, test.eq);
|
|
476
|
-
if ('in' in test)
|
|
477
|
-
return anyOf(test.in.map((value) => equal(held, value)));
|
|
478
|
-
if ('prefix' in test) {
|
|
479
|
-
if (!('value' in held) || typeof held.value !== 'string')
|
|
480
|
-
return undefined;
|
|
481
|
-
if (held.kind === 'number' || held.kind === 'reference' || held.kind === 'member')
|
|
482
|
-
return undefined;
|
|
483
|
-
return held.value.startsWith(test.prefix);
|
|
484
|
-
}
|
|
485
|
-
if ('gt' in test)
|
|
486
|
-
return ordered(held, test.gt, (sign) => sign > 0);
|
|
487
|
-
if ('gte' in test)
|
|
488
|
-
return ordered(held, test.gte, (sign) => sign >= 0);
|
|
489
|
-
if ('lt' in test)
|
|
490
|
-
return ordered(held, test.lt, (sign) => sign < 0);
|
|
491
|
-
return ordered(held, test.lte, (sign) => sign <= 0);
|
|
492
|
-
}
|
|
493
|
-
function leaf(test, read) {
|
|
494
|
-
if ('exists' in test) {
|
|
495
|
-
const present = read === undefined ? false : read.present;
|
|
496
|
-
return present === undefined ? undefined : present === test.exists;
|
|
497
|
-
}
|
|
498
|
-
if (read === undefined)
|
|
499
|
-
return undefined;
|
|
500
|
-
if ('contains' in test) {
|
|
501
|
-
if (read.items === null)
|
|
502
|
-
return undefined;
|
|
503
|
-
const truths = read.items.map((item) => item === undefined ? undefined : equal(item, test.contains));
|
|
504
|
-
return read.loose === true && !truths.includes(true) ? false : anyOf(truths);
|
|
505
|
-
}
|
|
506
|
-
if (read.values.length === 0)
|
|
507
|
-
return read === NO_ITEM ? false : undefined;
|
|
508
|
-
return anyOf(read.values.map((value) => (value === undefined ? undefined : compared(test, value))));
|
|
509
|
-
}
|
|
510
|
-
function evaluated(test, read) {
|
|
511
|
-
if ('all' in test)
|
|
512
|
-
return allOf(test.all.map((part) => evaluated(part, read)));
|
|
513
|
-
if ('any' in test)
|
|
514
|
-
return anyOf(test.any.map((part) => evaluated(part, read)));
|
|
515
|
-
if ('not' in test) {
|
|
516
|
-
const truth = evaluated(test.not, read);
|
|
517
|
-
return truth === undefined ? undefined : !truth;
|
|
518
|
-
}
|
|
519
|
-
return leaf(test, read(test.path));
|
|
520
|
-
}
|
|
521
|
-
export function members(document, path) {
|
|
522
|
-
if (!path.startsWith('/members/'))
|
|
523
|
-
return [];
|
|
524
|
-
const read = readDocument(document, path);
|
|
525
|
-
const values = read === undefined ? [] : [...read.values, ...(read.items ?? [])];
|
|
526
|
-
return [
|
|
527
|
-
...new Set(values.flatMap((value) => value !== undefined && value.kind === 'member' && 'value' in value
|
|
528
|
-
? [String(value.value)]
|
|
529
|
-
: [])),
|
|
530
|
-
].sort();
|
|
531
|
-
}
|
|
532
|
-
export function holds(test, document) {
|
|
533
|
-
return evaluated(test, (path) => readDocument(document, path));
|
|
534
|
-
}
|
|
535
|
-
function inScope(scope, document) {
|
|
536
|
-
if (document === null)
|
|
537
|
-
return false;
|
|
538
|
-
if ('type' in scope)
|
|
539
|
-
return document.type === scope.type;
|
|
540
|
-
if ('document' in scope)
|
|
541
|
-
return document.id === scope.document;
|
|
542
|
-
return scope.folder === '/' || document.path.startsWith(`${scope.folder}/`);
|
|
543
|
-
}
|
|
544
|
-
function matched(condition, test, document) {
|
|
545
|
-
if (!inScope(condition.scope, document))
|
|
546
|
-
return false;
|
|
547
|
-
return test === undefined || holds(test, document) === true;
|
|
548
|
-
}
|
|
549
|
-
function reachedAt(document, field) {
|
|
550
|
-
if (document === null)
|
|
551
|
-
return [];
|
|
552
|
-
if (field.startsWith('/sections/'))
|
|
553
|
-
return document.sections.includes(field.slice('/sections/'.length))
|
|
554
|
-
? [{ pointer: field, fact: PROSE }]
|
|
555
|
-
: [];
|
|
556
|
-
if (!field.startsWith('/fields/'))
|
|
557
|
-
return [];
|
|
558
|
-
return walked(rootOf(document, '/fields'), field.slice('/fields'.length)).reached;
|
|
559
|
-
}
|
|
560
|
-
function valueOf(fact) {
|
|
561
|
-
if (fact.kind === 'list')
|
|
562
|
-
return { kind: fact.kind, items: fact.items.map(valueOf) };
|
|
563
|
-
if (fact.kind === 'object')
|
|
564
|
-
return {
|
|
565
|
-
kind: fact.kind,
|
|
566
|
-
fields: Object.keys(fact.fields)
|
|
567
|
-
.sort()
|
|
568
|
-
.map((key) => [key, valueOf(fact.fields[key])]),
|
|
569
|
-
};
|
|
570
|
-
if (fact.kind === 'time') {
|
|
571
|
-
const at = instant(fact.value);
|
|
572
|
-
if (at !== undefined)
|
|
573
|
-
return { kind: fact.kind, at: String(at) };
|
|
574
|
-
}
|
|
575
|
-
return 'value' in fact ? { kind: fact.kind, value: fact.value } : fact;
|
|
576
|
-
}
|
|
577
|
-
function signature(reached) {
|
|
578
|
-
return JSON.stringify(reached.map(({ fact }) => valueOf(fact)));
|
|
579
|
-
}
|
|
580
|
-
function among(values, document, field) {
|
|
581
|
-
return holds({ path: field, in: values }, document) === true;
|
|
582
|
-
}
|
|
583
|
-
function fieldChanged(condition, event) {
|
|
584
|
-
const { field, from, to } = condition;
|
|
585
|
-
const before = reachedAt(event.before, field);
|
|
586
|
-
const after = reachedAt(event.after, field);
|
|
587
|
-
const pointers = [field, ...[...before, ...after].map(({ pointer }) => pointer)];
|
|
588
|
-
const prose = event.changed.some((changed) => pointers.some((pointer) => changed === pointer || changed.startsWith(`${pointer}/`)));
|
|
589
|
-
if (from === undefined && to === undefined && prose)
|
|
590
|
-
return true;
|
|
591
|
-
if (before.length === 0 && after.length === 0)
|
|
592
|
-
return false;
|
|
593
|
-
if (event.before !== null && event.after !== null && signature(before) === signature(after))
|
|
594
|
-
return false;
|
|
595
|
-
if (from === 'new'
|
|
596
|
-
? event.before !== null
|
|
597
|
-
: from !== undefined && (event.before === null || !among(from, event.before, field)))
|
|
598
|
-
return false;
|
|
599
|
-
if (to === 'deleted'
|
|
600
|
-
? event.after !== null
|
|
601
|
-
: to !== undefined && (event.after === null || !among(to, event.after, field)))
|
|
602
|
-
return false;
|
|
603
|
-
return true;
|
|
604
|
-
}
|
|
605
|
-
export function fires(condition, event) {
|
|
606
|
-
if (condition.moved_between_rooms !== undefined &&
|
|
607
|
-
condition.moved_between_rooms !== event.moved_between_rooms)
|
|
608
|
-
return false;
|
|
609
|
-
if (condition.by !== undefined && evaluated(condition.by, reader(ACTOR_TESTS, event.by)) !== true)
|
|
610
|
-
return false;
|
|
611
|
-
if (condition.mode === 'starts_matching') {
|
|
612
|
-
if (event.before === null && !condition.new_documents)
|
|
613
|
-
return false;
|
|
614
|
-
return (matched(condition, condition.match, event.after) &&
|
|
615
|
-
!matched(condition, condition.match, event.before));
|
|
616
|
-
}
|
|
617
|
-
const side = event.after ?? event.before;
|
|
618
|
-
return matched(condition, condition.match, side) && fieldChanged(condition, event);
|
|
619
|
-
}
|
|
620
|
-
export function itemFires(condition, item) {
|
|
621
|
-
if (condition.source !== undefined && item.source !== condition.source)
|
|
622
|
-
return false;
|
|
623
|
-
if (condition.address !== undefined && item.address !== condition.address)
|
|
624
|
-
return false;
|
|
625
|
-
return (condition.match === undefined || evaluated(condition.match, reader(ITEM_TESTS, item)) === true);
|
|
626
|
-
}
|
|
627
|
-
export function notificationFires(condition, notification) {
|
|
628
|
-
return condition.match === undefined || holds(condition.match, notification) === true;
|
|
629
|
-
}
|
|
630
|
-
export function messageFires(condition, message) {
|
|
631
|
-
return (condition.match === undefined ||
|
|
632
|
-
evaluated(condition.match, reader(MESSAGE_TESTS, message)) === true);
|
|
633
|
-
}
|