@thomasfosterau/pulpit 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +1271 -1
- package/dist/blobs.d.ts +249 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +193 -0
- package/dist/blobs.js.map +1 -0
- package/dist/budgets.d.ts +255 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +211 -0
- package/dist/budgets.js.map +1 -0
- package/dist/cache.d.ts +154 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +118 -0
- package/dist/cache.js.map +1 -0
- package/dist/collections.d.ts +242 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +215 -0
- package/dist/collections.js.map +1 -0
- package/dist/comments.d.ts +212 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +192 -0
- package/dist/comments.js.map +1 -0
- package/dist/content.d.ts +231 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +204 -0
- package/dist/content.js.map +1 -0
- package/dist/documents.d.ts +184 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +190 -0
- package/dist/documents.js.map +1 -0
- package/dist/edges.d.ts +645 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +558 -0
- package/dist/edges.js.map +1 -0
- package/dist/filter.d.ts +461 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +387 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/filter-columns.d.ts +8 -0
- package/dist/internal/filter-columns.d.ts.map +1 -0
- package/dist/internal/filter-columns.js +43 -0
- package/dist/internal/filter-columns.js.map +1 -0
- package/dist/internal/list-order.d.ts +26 -0
- package/dist/internal/list-order.d.ts.map +1 -0
- package/dist/internal/list-order.js +92 -0
- package/dist/internal/list-order.js.map +1 -0
- package/dist/internal/owned-columns.d.ts +93 -0
- package/dist/internal/owned-columns.d.ts.map +1 -0
- package/dist/internal/owned-columns.js +98 -0
- package/dist/internal/owned-columns.js.map +1 -0
- package/dist/internal/plain-rows.d.ts +32 -0
- package/dist/internal/plain-rows.d.ts.map +1 -0
- package/dist/internal/plain-rows.js +32 -0
- package/dist/internal/plain-rows.js.map +1 -0
- package/dist/internal/reference-columns.d.ts +80 -0
- package/dist/internal/reference-columns.d.ts.map +1 -0
- package/dist/internal/reference-columns.js +102 -0
- package/dist/internal/reference-columns.js.map +1 -0
- package/dist/moderation.d.ts +392 -0
- package/dist/moderation.d.ts.map +1 -0
- package/dist/moderation.js +375 -0
- package/dist/moderation.js.map +1 -0
- package/dist/paths.d.ts +204 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +165 -0
- package/dist/paths.js.map +1 -0
- package/dist/proposals.d.ts +523 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +505 -0
- package/dist/proposals.js.map +1 -0
- package/dist/publication.d.ts +148 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +183 -0
- package/dist/publication.js.map +1 -0
- package/dist/purge.d.ts +224 -0
- package/dist/purge.d.ts.map +1 -0
- package/dist/purge.js +151 -0
- package/dist/purge.js.map +1 -0
- package/dist/reactions.d.ts +246 -0
- package/dist/reactions.d.ts.map +1 -0
- package/dist/reactions.js +183 -0
- package/dist/reactions.js.map +1 -0
- package/dist/registry.d.ts +316 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +305 -0
- package/dist/registry.js.map +1 -0
- package/dist/search.d.ts +366 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +273 -0
- package/dist/search.js.map +1 -0
- package/dist/social.d.ts +311 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +205 -0
- package/dist/social.js.map +1 -0
- package/dist/subtype.d.ts +1613 -0
- package/dist/subtype.d.ts.map +1 -0
- package/dist/subtype.js +1300 -0
- package/dist/subtype.js.map +1 -0
- package/dist/testing.d.ts +675 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +1022 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-chain.d.ts +702 -0
- package/dist/version-chain.d.ts.map +1 -0
- package/dist/version-chain.js +489 -0
- package/dist/version-chain.js.map +1 -0
- package/dist/working-copy.d.ts +116 -0
- package/dist/working-copy.d.ts.map +1 -0
- package/dist/working-copy.js +95 -0
- package/dist/working-copy.js.map +1 -0
- package/package.json +138 -4
- package/src/blobs.ts +384 -0
- package/src/budgets.ts +338 -0
- package/src/cache.ts +218 -0
- package/src/collections.ts +427 -0
- package/src/comments.ts +392 -0
- package/src/content.ts +238 -0
- package/src/documents.ts +284 -0
- package/src/edges.ts +1185 -0
- package/src/filter.ts +523 -0
- package/src/index.ts +153 -0
- package/src/internal/filter-columns.ts +50 -0
- package/src/internal/list-order.ts +117 -0
- package/src/internal/owned-columns.ts +140 -0
- package/src/internal/plain-rows.ts +54 -0
- package/src/internal/reference-columns.ts +156 -0
- package/src/moderation.ts +728 -0
- package/src/paths.ts +267 -0
- package/src/proposals.ts +1014 -0
- package/src/publication.ts +225 -0
- package/src/purge.ts +313 -0
- package/src/reactions.ts +347 -0
- package/src/registry.ts +524 -0
- package/src/search.ts +540 -0
- package/src/social.ts +469 -0
- package/src/subtype.ts +3095 -0
- package/src/testing.ts +1873 -0
- package/src/version-chain.ts +1093 -0
- package/src/working-copy.ts +192 -0
package/src/filter.ts
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The list-filter vocabulary** — the kernel's read predicates as data.
|
|
3
|
+
*
|
|
4
|
+
* Every visibility, soft-delete and publication rule the kernel owns has two
|
|
5
|
+
* readings: a per-row answer (may this viewer see this row?) and a set-shaped
|
|
6
|
+
* answer a LIST turns into its `WHERE`. In the apps the second is Drizzle SQL
|
|
7
|
+
* built inline (`ownedListFilterE`, `loadListVisibilityFilters`,
|
|
8
|
+
* `publishedFilter`), which ties the rule to one database layer and makes the
|
|
9
|
+
* two readings impossible to compare. Here the set-shaped answer is a small
|
|
10
|
+
* predicate tree, {@link ListFilter}: a Drizzle renderer (the seam
|
|
11
|
+
* effect-drizzle-ext / effect-auth-drizzle fill, see the package README →
|
|
12
|
+
* Seams) turns it into SQL, and
|
|
13
|
+
* {@link evaluate} runs it against an in-memory row — which is what lets the
|
|
14
|
+
* unit suite prove the list filter and the row predicate never disagree.
|
|
15
|
+
*
|
|
16
|
+
* `Following` is whatever a seam hands the `in` node: the follower seam's
|
|
17
|
+
* set of followed accounts, or an edge family's members
|
|
18
|
+
* (`EdgeStore.members`, pulpit#113); a subquery (`SQLWrapper`) for SQL, a
|
|
19
|
+
* set for the in-memory evaluator. The `notIn` node takes the same
|
|
20
|
+
* `Following`, for an exclusion (`EdgeOps.nonMemberFilter`, pulpit#191).
|
|
21
|
+
*
|
|
22
|
+
* Rendering contract a SQL renderer must honour:
|
|
23
|
+
*
|
|
24
|
+
* | node | SQL |
|
|
25
|
+
* | --- | --- |
|
|
26
|
+
* | `always` | no predicate (`undefined` to Drizzle's `.where`) |
|
|
27
|
+
* | `never` | a predicate no row satisfies (the apps: `id = '00000000-…'`) |
|
|
28
|
+
* | `eq` / `ne` | `col = $v` / `col <> $v` — a `NULL` column satisfies neither |
|
|
29
|
+
* | `in` | `col IN (<seam subquery>)` — a subquery, never a materialised array (#1107); a `NULL` column satisfies none |
|
|
30
|
+
* | `notIn` | `(col IS NULL OR col NOT IN (<seam subquery>))` — the complement of `in` over the same subquery (pulpit#191); a `NULL` column satisfies it, and the subquery must never yield a `NULL` (`NOT IN` over a set holding one is `NULL` for every row) |
|
|
31
|
+
* | `gt` / `gte` / `lt` / `lte` | `col > $v` / `col >= $v` / `col < $v` / `col <= $v` — a string compared with `COLLATE "C"` (code-point order), a `Date` as a timestamp, a number numerically (`±Infinity` included); a `NULL` column satisfies none |
|
|
32
|
+
* | `anyOf` | `col = ANY($values)` — a `NULL` column satisfies none; empty is `never`; never a `NULL` element |
|
|
33
|
+
* | `noneOf` | `col <> ALL($values)` — a `NULL` column satisfies none; empty is `always` (`<> ALL` over no values is true); never a `NULL` element (`<> ALL('{1,NULL}')` is `NULL` for every row outside the set) |
|
|
34
|
+
* | `isNull` | `col IS NULL` — the one node a `NULL` column satisfies by itself |
|
|
35
|
+
* | `published` | `(col IS NOT NULL AND col <= now())` — the database's clock, one per query |
|
|
36
|
+
* | `notPublished` | `(col IS NULL OR col > now())` — parenthesised, so it cannot bind to a surrounding `AND` |
|
|
37
|
+
* | `locale` | `col = ANY($locales)` over the view's locale column (pulpit#56) — as `anyOf`; on a view with no locale level, `never` |
|
|
38
|
+
* | `and` / `or` | `AND` / `OR` over the parts |
|
|
39
|
+
*
|
|
40
|
+
* An `anyOf` / `noneOf` node carrying more than {@link MAX_SET_VALUES} values,
|
|
41
|
+
* duplicates counted, is `never`, in the renderer as in {@link evaluate}
|
|
42
|
+
* (`SEC-INPUT-003`); the constructors already fold it so.
|
|
43
|
+
*
|
|
44
|
+
* Outside the contract, where SQL and {@link evaluate} cannot be made to
|
|
45
|
+
* agree, `evaluate` fails closed:
|
|
46
|
+
*
|
|
47
|
+
* - **A column of another type than the comparand** (or than any of a
|
|
48
|
+
* literal set's values) is a caller or schema error. SQL may coerce the
|
|
49
|
+
* parameter (an `integer` column against `'1'`) or raise (against
|
|
50
|
+
* `'abc'`); `evaluate` reads every such row as satisfying no `eq`, `ne`,
|
|
51
|
+
* comparison, `anyOf` or `noneOf`.
|
|
52
|
+
* - **`NaN`** (and an invalid `Date`). PostgreSQL's `float8` / `numeric` hold
|
|
53
|
+
* `NaN` equal to itself and above every number, an order JavaScript's
|
|
54
|
+
* `NaN` does not have. The constructors fold a `NaN` comparand, or a set
|
|
55
|
+
* holding one, to `never`; `evaluate` reads a `NaN` comparand or column as
|
|
56
|
+
* satisfying no `eq`, `ne`, comparison, `anyOf` or `noneOf`; and a renderer
|
|
57
|
+
* must never bind `NaN`, rendering a hand-built node carrying one as `never`.
|
|
58
|
+
* - **Sub-millisecond instants.** A PostgreSQL timestamp keeps microseconds,
|
|
59
|
+
* a JavaScript `Date` milliseconds, so two instants within one millisecond
|
|
60
|
+
* can compare unequal in SQL and equal in `evaluate`.
|
|
61
|
+
*
|
|
62
|
+
* @copyright Thomas Foster
|
|
63
|
+
* @module pulpit/filter
|
|
64
|
+
* @since 0.1.0
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
import * as Array from "effect/Array";
|
|
68
|
+
import * as Match from "effect/Match";
|
|
69
|
+
import * as Order from "effect/Order";
|
|
70
|
+
import * as Predicate from "effect/Predicate";
|
|
71
|
+
import { byCodePoint } from "./internal/list-order.ts";
|
|
72
|
+
import type { ColumnValue } from "./version-chain.ts";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A value an `eq` / `ne` node compares against. No `null`: `col = NULL` is
|
|
76
|
+
* never true in SQL, so a null comparison would read differently in the
|
|
77
|
+
* renderer and the evaluator.
|
|
78
|
+
*
|
|
79
|
+
* @since 0.1.0
|
|
80
|
+
*/
|
|
81
|
+
export type Scalar = string | number | boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A value a `gt` / `gte` / `lt` / `lte` node compares against. A `string`
|
|
85
|
+
* orders by code point (SQL's `"C"` collation), a `number` numerically and a
|
|
86
|
+
* `Date` by its instant. Kept apart from {@link Scalar}, which `eq` / `ne`
|
|
87
|
+
* test by identity and so cannot hold a `Date`; a `boolean` has no order a
|
|
88
|
+
* filter needs.
|
|
89
|
+
*
|
|
90
|
+
* @since 0.1.0
|
|
91
|
+
*/
|
|
92
|
+
export type Comparable = string | number | Date;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The most values an `anyOf` / `noneOf` node carries (`SEC-INPUT-003`): a
|
|
96
|
+
* literal set past it is `never`, so no request drives an unbounded
|
|
97
|
+
* `= ANY($values)` parameter. An application's own bound sits below it:
|
|
98
|
+
* `@thomasfosterau/pulpit-wordpress` caps each list parameter at 100, a cap
|
|
99
|
+
* WordPress core itself doesn't set (pulpit#86).
|
|
100
|
+
*
|
|
101
|
+
* @since 0.1.0
|
|
102
|
+
*/
|
|
103
|
+
export const MAX_SET_VALUES = 1000;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A row as {@link evaluate} reads it: the view's columns by name.
|
|
107
|
+
*
|
|
108
|
+
* @since 0.1.0
|
|
109
|
+
*/
|
|
110
|
+
export type FilterRow = { readonly [column: string]: ColumnValue };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A read predicate over a current-version view's columns.
|
|
114
|
+
*
|
|
115
|
+
* @since 0.1.0
|
|
116
|
+
*/
|
|
117
|
+
export type ListFilter<Following> =
|
|
118
|
+
| { readonly kind: "always" }
|
|
119
|
+
| { readonly kind: "never" }
|
|
120
|
+
| { readonly kind: "eq"; readonly column: string; readonly value: Scalar }
|
|
121
|
+
| { readonly kind: "ne"; readonly column: string; readonly value: Scalar }
|
|
122
|
+
| { readonly kind: "in"; readonly column: string; readonly set: Following }
|
|
123
|
+
| { readonly kind: "notIn"; readonly column: string; readonly set: Following }
|
|
124
|
+
| { readonly kind: "gt"; readonly column: string; readonly value: Comparable }
|
|
125
|
+
| { readonly kind: "gte"; readonly column: string; readonly value: Comparable }
|
|
126
|
+
| { readonly kind: "lt"; readonly column: string; readonly value: Comparable }
|
|
127
|
+
| { readonly kind: "lte"; readonly column: string; readonly value: Comparable }
|
|
128
|
+
| { readonly kind: "anyOf"; readonly column: string; readonly values: ReadonlyArray<Scalar> }
|
|
129
|
+
| { readonly kind: "noneOf"; readonly column: string; readonly values: ReadonlyArray<Scalar> }
|
|
130
|
+
| { readonly kind: "isNull"; readonly column: string }
|
|
131
|
+
| { readonly kind: "published"; readonly column: string }
|
|
132
|
+
| { readonly kind: "notPublished"; readonly column: string }
|
|
133
|
+
| { readonly kind: "locale"; readonly column: string; readonly locales: ReadonlyArray<string> }
|
|
134
|
+
| { readonly kind: "and"; readonly filters: ReadonlyArray<ListFilter<Following>> }
|
|
135
|
+
| { readonly kind: "or"; readonly filters: ReadonlyArray<ListFilter<Following>> };
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* No predicate: every row.
|
|
139
|
+
*
|
|
140
|
+
* @since 0.1.0
|
|
141
|
+
*/
|
|
142
|
+
export const always = { kind: "always" } as const;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Fail closed: no row.
|
|
146
|
+
*
|
|
147
|
+
* @since 0.1.0
|
|
148
|
+
*/
|
|
149
|
+
export const never = { kind: "never" } as const;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A `NaN`, or an invalid `Date`: a value with no place in the order SQL and
|
|
153
|
+
* {@link evaluate} share (see the module's "Outside the contract").
|
|
154
|
+
*/
|
|
155
|
+
const isUnordered = (value: ColumnValue): boolean =>
|
|
156
|
+
(Predicate.isNumber(value) && Number.isNaN(value)) ||
|
|
157
|
+
(Predicate.isDate(value) && Number.isNaN(value.getTime()));
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* `column = value`. A `NaN` value is {@link never}: PostgreSQL's `NaN = NaN`
|
|
161
|
+
* is true where JavaScript's is not, so it fails closed.
|
|
162
|
+
*
|
|
163
|
+
* @since 0.1.0
|
|
164
|
+
*/
|
|
165
|
+
export const eq = (column: string, value: Scalar) =>
|
|
166
|
+
isUnordered(value) ? never : ({ kind: "eq", column, value } as const);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `column <> value`. A `NULL` or absent column satisfies it no more than it
|
|
170
|
+
* satisfies {@link eq}: SQL's `NULL <> $v` is `NULL`, not true. A `NaN`
|
|
171
|
+
* value is {@link never}, failing closed.
|
|
172
|
+
*
|
|
173
|
+
* @since 0.1.0
|
|
174
|
+
*/
|
|
175
|
+
export const ne = (column: string, value: Scalar) =>
|
|
176
|
+
isUnordered(value) ? never : ({ kind: "ne", column, value } as const);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `column IN (following)`. A `NULL` or absent column satisfies none, whatever
|
|
180
|
+
* `following` holds: SQL's `NULL IN (…)` is `NULL`, not true.
|
|
181
|
+
*
|
|
182
|
+
* @since 0.1.0
|
|
183
|
+
*/
|
|
184
|
+
export const isIn = <Following>(column: string, set: Following) =>
|
|
185
|
+
({ kind: "in", column, set }) as const;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `(column IS NULL OR column NOT IN (following))` (pulpit#191): the exact
|
|
189
|
+
* complement of {@link isIn} over the same seam set, so `isIn(c, s)` and
|
|
190
|
+
* `isNotIn(c, s)` partition every row. A `NULL` or absent column satisfies
|
|
191
|
+
* it, whatever `following` holds, and is never asked about; the seam's set
|
|
192
|
+
* must never hold a `NULL` (SQL's `NOT IN` over one is `NULL` for every row).
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* import { strictEqual } from "node:assert";
|
|
197
|
+
* import { evaluate, isNotIn } from "@thomasfosterau/pulpit/filter";
|
|
198
|
+
*
|
|
199
|
+
* const excluded = new Set(["a"]);
|
|
200
|
+
* const context = { now: new Date(0), contains: (set: Set<string>, value: unknown) => set.has(String(value)) };
|
|
201
|
+
* strictEqual(evaluate(isNotIn("id", excluded), { id: "a" }, context), false);
|
|
202
|
+
* strictEqual(evaluate(isNotIn("id", excluded), { id: "b" }, context), true);
|
|
203
|
+
* strictEqual(evaluate(isNotIn("id", excluded), { id: null }, context), true);
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @since 0.1.0
|
|
207
|
+
*/
|
|
208
|
+
export const isNotIn = <Following>(column: string, set: Following) =>
|
|
209
|
+
({ kind: "notIn", column, set }) as const;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `column > value`. A `NULL` or absent column satisfies it no more than SQL's
|
|
213
|
+
* `NULL > $v` (which is `NULL`); `evaluate` reads a column holding another
|
|
214
|
+
* type than `value` as satisfying none, too. A `NaN` or invalid-`Date` value
|
|
215
|
+
* is {@link never}. See {@link Comparable} for the orders.
|
|
216
|
+
*
|
|
217
|
+
* @since 0.1.0
|
|
218
|
+
*/
|
|
219
|
+
export const gt = (column: string, value: Comparable) =>
|
|
220
|
+
isUnordered(value) ? never : ({ kind: "gt", column, value } as const);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* `column >= value`; equality satisfies it, `Infinity` against `Infinity`
|
|
224
|
+
* included. A `NULL` or absent column satisfies none (SQL's `NULL >= $v` is
|
|
225
|
+
* `NULL`); `evaluate` reads a column holding another type than `value` as
|
|
226
|
+
* satisfying none, too. A `NaN` or invalid-`Date` value is {@link never}.
|
|
227
|
+
*
|
|
228
|
+
* @since 0.1.0
|
|
229
|
+
*/
|
|
230
|
+
export const gte = (column: string, value: Comparable) =>
|
|
231
|
+
isUnordered(value) ? never : ({ kind: "gte", column, value } as const);
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* `column < value`. A `NULL` or absent column satisfies none (SQL's
|
|
235
|
+
* `NULL < $v` is `NULL`); `evaluate` reads a column holding another type than
|
|
236
|
+
* `value` as satisfying none, too. A `NaN` or invalid-`Date` value is
|
|
237
|
+
* {@link never}.
|
|
238
|
+
*
|
|
239
|
+
* @since 0.1.0
|
|
240
|
+
*/
|
|
241
|
+
export const lt = (column: string, value: Comparable) =>
|
|
242
|
+
isUnordered(value) ? never : ({ kind: "lt", column, value } as const);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `column <= value`; equality satisfies it, `-Infinity` against `-Infinity`
|
|
246
|
+
* included. A `NULL` or absent column satisfies none (SQL's `NULL <= $v` is
|
|
247
|
+
* `NULL`); `evaluate` reads a column holding another type than `value` as
|
|
248
|
+
* satisfying none, too. A `NaN` or invalid-`Date` value is {@link never}.
|
|
249
|
+
*
|
|
250
|
+
* @since 0.1.0
|
|
251
|
+
*/
|
|
252
|
+
export const lte = (column: string, value: Comparable) =>
|
|
253
|
+
isUnordered(value) ? never : ({ kind: "lte", column, value } as const);
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* `column = ANY(values)`: the column equals one of a literal set, each tested
|
|
257
|
+
* as {@link eq} tests its value. A `NULL` or absent column is in no set. No
|
|
258
|
+
* values is {@link never}, and so are more than {@link MAX_SET_VALUES}
|
|
259
|
+
* (duplicates counted, `SEC-INPUT-003`) and a set holding a `NaN`, so
|
|
260
|
+
* {@link and} / {@link or} fold each. This is a literal set; a follower
|
|
261
|
+
* subquery is {@link isIn}'s.
|
|
262
|
+
*
|
|
263
|
+
* @since 0.1.0
|
|
264
|
+
*/
|
|
265
|
+
export const anyOf = (column: string, values: ReadonlyArray<Scalar>) =>
|
|
266
|
+
Array.isReadonlyArrayEmpty(values) || values.length > MAX_SET_VALUES || values.some(isUnordered)
|
|
267
|
+
? never
|
|
268
|
+
: ({ kind: "anyOf", column, values: [...values] } as const);
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* `column <> ALL(values)`: the column equals none of a literal set, each
|
|
272
|
+
* tested as {@link ne} tests its value. A `NULL` or absent column satisfies it
|
|
273
|
+
* no more than it satisfies `ne` (SQL's `NULL <> ALL(…)` is `NULL`). No values
|
|
274
|
+
* is {@link always}; more than {@link MAX_SET_VALUES} (duplicates counted) is
|
|
275
|
+
* {@link never}, failing closed rather than excluding only some
|
|
276
|
+
* (`SEC-INPUT-003`), and so is a set holding a `NaN`.
|
|
277
|
+
*
|
|
278
|
+
* @since 0.1.0
|
|
279
|
+
*/
|
|
280
|
+
export const noneOf = (column: string, values: ReadonlyArray<Scalar>) =>
|
|
281
|
+
Array.isReadonlyArrayEmpty(values)
|
|
282
|
+
? always
|
|
283
|
+
: values.length > MAX_SET_VALUES || values.some(isUnordered)
|
|
284
|
+
? never
|
|
285
|
+
: ({ kind: "noneOf", column, values: [...values] } as const);
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* `column IS NULL`: the column holds no value (`evaluate` reads an absent
|
|
289
|
+
* column so, too). The one node a `NULL` column satisfies on its own, so
|
|
290
|
+
* `or(isNull(column), noneOf(column, values))` is the exclusion that keeps an
|
|
291
|
+
* unset column, which a bare {@link noneOf} drops (pulpit#170).
|
|
292
|
+
*
|
|
293
|
+
* @since 0.1.0
|
|
294
|
+
*/
|
|
295
|
+
export const isNull = (column: string) => ({ kind: "isNull", column }) as const;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Stamped, and the stamp has arrived.
|
|
299
|
+
*
|
|
300
|
+
* @since 0.1.0
|
|
301
|
+
*/
|
|
302
|
+
export const published = (column: string) => ({ kind: "published", column }) as const;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Not out: unstamped, or stamped in the future — the exact complement of {@link published}.
|
|
306
|
+
*
|
|
307
|
+
* @since 0.1.0
|
|
308
|
+
*/
|
|
309
|
+
export const notPublished = (column: string) => ({ kind: "notPublished", column }) as const;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The column a localised leaf's current-version view keys its locale level
|
|
313
|
+
* by (pulpit#56): one view row per `(id, locale)` the head carries, the
|
|
314
|
+
* shared columns joined in, and a node carrying no locale as one row whose
|
|
315
|
+
* `locale` is `NULL`. No leaf column may take the name.
|
|
316
|
+
*
|
|
317
|
+
* @since 0.1.0
|
|
318
|
+
*/
|
|
319
|
+
export const LOCALE_COLUMN = "locale";
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The locale arm (pulpit#56): the view rows in one of `locales`, read from
|
|
323
|
+
* the view's {@link LOCALE_COLUMN} as {@link anyOf} reads its column. A row
|
|
324
|
+
* carrying no locale (a `NULL` locale), and every row of a view with no
|
|
325
|
+
* locale level, is in none. No locales is {@link never}, and so are more than
|
|
326
|
+
* {@link MAX_SET_VALUES} (`SEC-INPUT-003`).
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* import { strictEqual } from "node:assert";
|
|
331
|
+
* import { evaluate, locale } from "@thomasfosterau/pulpit/filter";
|
|
332
|
+
*
|
|
333
|
+
* const context = { now: new Date(0), contains: () => false };
|
|
334
|
+
* strictEqual(evaluate(locale(["fr", "en"]), { id: "a", locale: "fr" }, context), true);
|
|
335
|
+
* strictEqual(evaluate(locale(["fr", "en"]), { id: "a", locale: "de" }, context), false);
|
|
336
|
+
* strictEqual(evaluate(locale(["fr"]), { id: "a" }, context), false);
|
|
337
|
+
* ```
|
|
338
|
+
*
|
|
339
|
+
* @since 0.1.0
|
|
340
|
+
*/
|
|
341
|
+
export const locale = (locales: ReadonlyArray<string>) =>
|
|
342
|
+
Array.isReadonlyArrayEmpty(locales) || locales.length > MAX_SET_VALUES
|
|
343
|
+
? never
|
|
344
|
+
: ({ kind: "locale", column: LOCALE_COLUMN, locales: [...locales] } as const);
|
|
345
|
+
|
|
346
|
+
/** No parts → the identity; one part → that part, bare; more → the combined node. */
|
|
347
|
+
const collapse = <Following>(
|
|
348
|
+
parts: ReadonlyArray<ListFilter<Following>>,
|
|
349
|
+
identity: ListFilter<Following>,
|
|
350
|
+
combine: (parts: ReadonlyArray<ListFilter<Following>>) => ListFilter<Following>,
|
|
351
|
+
): ListFilter<Following> =>
|
|
352
|
+
Array.match(parts, {
|
|
353
|
+
onEmpty: () => identity,
|
|
354
|
+
onNonEmpty: (nonEmpty) =>
|
|
355
|
+
Array.isReadonlyArrayNonEmpty(Array.tailNonEmpty(nonEmpty))
|
|
356
|
+
? combine(nonEmpty)
|
|
357
|
+
: Array.headNonEmpty(nonEmpty),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Conjunction. `always` parts drop out, a `never` part makes the whole
|
|
362
|
+
* `never`, and a single remaining part is returned bare — so composing
|
|
363
|
+
* optional filters reads as one call, the shape the apps' `combine` had.
|
|
364
|
+
*
|
|
365
|
+
* @since 0.1.0
|
|
366
|
+
*/
|
|
367
|
+
export const and = <Following>(
|
|
368
|
+
...filters: ReadonlyArray<ListFilter<Following>>
|
|
369
|
+
): ListFilter<Following> => {
|
|
370
|
+
if (filters.some((filter) => filter.kind === "never")) return never;
|
|
371
|
+
return collapse(
|
|
372
|
+
Array.filter(filters, (filter) => filter.kind !== "always"),
|
|
373
|
+
always,
|
|
374
|
+
(parts) => ({ kind: "and", filters: parts }),
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Disjunction. `never` parts drop out, an `always` part makes the whole
|
|
380
|
+
* `always`, and a single remaining part is returned bare.
|
|
381
|
+
*
|
|
382
|
+
* @since 0.1.0
|
|
383
|
+
*/
|
|
384
|
+
export const or = <Following>(
|
|
385
|
+
...filters: ReadonlyArray<ListFilter<Following>>
|
|
386
|
+
): ListFilter<Following> => {
|
|
387
|
+
if (filters.some((filter) => filter.kind === "always")) return always;
|
|
388
|
+
return collapse(
|
|
389
|
+
Array.filter(filters, (filter) => filter.kind !== "never"),
|
|
390
|
+
never,
|
|
391
|
+
(parts) => ({ kind: "or", filters: parts }),
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* What {@link evaluate} needs besides the row: the query's clock and the `in` test.
|
|
397
|
+
*
|
|
398
|
+
* @since 0.1.0
|
|
399
|
+
*/
|
|
400
|
+
export interface EvaluationContext<Following> {
|
|
401
|
+
/** The instant `published` / `notPublished` compare against — the query's `now()`. */
|
|
402
|
+
readonly now: Date;
|
|
403
|
+
/**
|
|
404
|
+
* Whether `value` is a member of the follower seam's `set`. {@link evaluate}
|
|
405
|
+
* never asks it about a `null` or absent column: such a row satisfies no
|
|
406
|
+
* `in`, and every `notIn`, whatever this would answer.
|
|
407
|
+
*/
|
|
408
|
+
readonly contains: (set: Following, value: ColumnValue) => boolean;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Numbers by `<` / `>`, never by subtraction: `Infinity - Infinity` is `NaN`,
|
|
413
|
+
* which would read `Infinity >= Infinity` as false. Either side `NaN` (or an
|
|
414
|
+
* invalid `Date`'s instant) is `NaN`, which every relational test reads as false.
|
|
415
|
+
*/
|
|
416
|
+
const byNumber = (column: number, value: number): number =>
|
|
417
|
+
Number.isNaN(column) || Number.isNaN(value) ? Number.NaN : Order.Number(column, value);
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Compare a column with a {@link Comparable}: negative, zero or positive, or
|
|
421
|
+
* `NaN` when the column is `NULL`, absent, of another type or unordered
|
|
422
|
+
* (`NaN`, an invalid `Date`), which every relational test then reads as
|
|
423
|
+
* false, as SQL's `NULL` is.
|
|
424
|
+
*/
|
|
425
|
+
const compareTo = (column: ColumnValue, value: Comparable): number => {
|
|
426
|
+
if (Predicate.isDate(value)) {
|
|
427
|
+
return Predicate.isDate(column) ? byNumber(column.getTime(), value.getTime()) : Number.NaN;
|
|
428
|
+
}
|
|
429
|
+
if (Predicate.isString(value)) {
|
|
430
|
+
return Predicate.isString(column) ? byCodePoint(column, value) : Number.NaN;
|
|
431
|
+
}
|
|
432
|
+
return Predicate.isNumber(column) ? byNumber(column, value) : Number.NaN;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* `column <> value` as SQL reads it where the two readings agree: a `NULL`
|
|
437
|
+
* column differs from nothing (`NULL <> $v` is `NULL`), and outside the
|
|
438
|
+
* contract — a column of another type than `value`, or a `NaN` on either
|
|
439
|
+
* side — it fails closed rather than answer true.
|
|
440
|
+
*/
|
|
441
|
+
const differs = (column: ColumnValue, value: Scalar): boolean =>
|
|
442
|
+
sameKind(column, value) && !isUnordered(column) && !isUnordered(value) && column !== value;
|
|
443
|
+
|
|
444
|
+
/** Whether `column` holds the same kind of {@link Scalar} as `value`. */
|
|
445
|
+
const sameKind = (column: ColumnValue, value: Scalar): boolean =>
|
|
446
|
+
Predicate.isString(value)
|
|
447
|
+
? Predicate.isString(column)
|
|
448
|
+
: Predicate.isNumber(value)
|
|
449
|
+
? Predicate.isNumber(column)
|
|
450
|
+
: Predicate.isBoolean(column);
|
|
451
|
+
|
|
452
|
+
/** Read a timestamp column the way the publication axis does: a `Date`, or nothing. */
|
|
453
|
+
const instantOf = (value: ColumnValue): number =>
|
|
454
|
+
value instanceof Date ? value.getTime() : Number.NaN;
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Run a filter against one row — the in-memory reading of the rendering
|
|
458
|
+
* contract above. A column that is `null` or that the row lacks matches no
|
|
459
|
+
* `eq`, satisfies no `ne`, `in`, comparison, `anyOf` or non-empty `noneOf`,
|
|
460
|
+
* satisfies every `notIn`, and is unpublished: SQL reads `NULL = $v`, `NULL <> $v`, `NULL IN (…)`,
|
|
461
|
+
* `NULL > $v` and `NULL <> ALL(…)` as `NULL`, which a `WHERE` drops, so `ne`
|
|
462
|
+
* is not the complement of `eq`, nor `noneOf` of `anyOf`, over a nullable
|
|
463
|
+
* column; {@link isNull} is the one node such a row satisfies. Such a row fails an `in` without `context.contains` being asked, so
|
|
464
|
+
* the answer never depends on the seam (pulpit#94). A set past
|
|
465
|
+
* {@link MAX_SET_VALUES} is `never`. Outside the contract it fails closed: a
|
|
466
|
+
* column of another type than the comparand (or than any of a set's values),
|
|
467
|
+
* and a `NaN` (or invalid `Date`) comparand, set element or column, satisfy
|
|
468
|
+
* no `eq`, `ne`, comparison, `anyOf` or non-empty `noneOf`.
|
|
469
|
+
*
|
|
470
|
+
* @since 0.1.0
|
|
471
|
+
*/
|
|
472
|
+
export const evaluate = <Following>(
|
|
473
|
+
filter: ListFilter<Following>,
|
|
474
|
+
row: FilterRow,
|
|
475
|
+
context: EvaluationContext<Following>,
|
|
476
|
+
): boolean => {
|
|
477
|
+
const arrived = (column: string) => instantOf(row[column] ?? null) <= context.now.getTime();
|
|
478
|
+
return Match.value(filter).pipe(
|
|
479
|
+
Match.discriminatorsExhaustive("kind")({
|
|
480
|
+
always: () => true,
|
|
481
|
+
never: () => false,
|
|
482
|
+
// `NaN === NaN` is false, so a `NaN` on either side already matches nothing.
|
|
483
|
+
eq: (node) => row[node.column] === node.value,
|
|
484
|
+
ne: (node) => differs(row[node.column] ?? null, node.value),
|
|
485
|
+
in: (node) => {
|
|
486
|
+
const value = row[node.column] ?? null;
|
|
487
|
+
return value !== null && context.contains(node.set, value);
|
|
488
|
+
},
|
|
489
|
+
notIn: (node) => {
|
|
490
|
+
const value = row[node.column] ?? null;
|
|
491
|
+
return value === null || !context.contains(node.set, value);
|
|
492
|
+
},
|
|
493
|
+
gt: (node) => compareTo(row[node.column] ?? null, node.value) > 0,
|
|
494
|
+
gte: (node) => compareTo(row[node.column] ?? null, node.value) >= 0,
|
|
495
|
+
lt: (node) => compareTo(row[node.column] ?? null, node.value) < 0,
|
|
496
|
+
lte: (node) => compareTo(row[node.column] ?? null, node.value) <= 0,
|
|
497
|
+
anyOf: (node) =>
|
|
498
|
+
node.values.length <= MAX_SET_VALUES &&
|
|
499
|
+
!node.values.some(isUnordered) &&
|
|
500
|
+
node.values.some((value) => row[node.column] === value),
|
|
501
|
+
noneOf: (node) => {
|
|
502
|
+
if (node.values.length > MAX_SET_VALUES) return false;
|
|
503
|
+
// `<> ALL` over no values is true, even for NULL; the constructor folds that to `always`.
|
|
504
|
+
if (Array.isReadonlyArrayEmpty(node.values)) return true;
|
|
505
|
+
const value = row[node.column] ?? null;
|
|
506
|
+
return node.values.every((excluded) => differs(value, excluded));
|
|
507
|
+
},
|
|
508
|
+
isNull: (node) => (row[node.column] ?? null) === null,
|
|
509
|
+
published: (node) => arrived(node.column),
|
|
510
|
+
notPublished: (node) => !arrived(node.column),
|
|
511
|
+
locale: (node) => {
|
|
512
|
+
const value = row[node.column] ?? null;
|
|
513
|
+
return (
|
|
514
|
+
node.locales.length <= MAX_SET_VALUES &&
|
|
515
|
+
Predicate.isString(value) &&
|
|
516
|
+
node.locales.includes(value)
|
|
517
|
+
);
|
|
518
|
+
},
|
|
519
|
+
and: (node) => node.filters.every((part) => evaluate(part, row, context)),
|
|
520
|
+
or: (node) => node.filters.some((part) => evaluate(part, row, context)),
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/pulpit` — the versioned-content kernel.
|
|
3
|
+
*
|
|
4
|
+
* Every module is exported as a namespace from the package root, following
|
|
5
|
+
* the convention of `effect` and `@thomasfosterau/effect-jsonapi`; each is
|
|
6
|
+
* also a subpath (`@thomasfosterau/pulpit/content`, …) for a consumer that
|
|
7
|
+
* wants one module without the barrel. The in-memory fakes are the one
|
|
8
|
+
* exception: they are the subpath `@thomasfosterau/pulpit/testing` only, so
|
|
9
|
+
* that production code never pulls them in through the barrel.
|
|
10
|
+
*
|
|
11
|
+
* @copyright Thomas Foster
|
|
12
|
+
* @module pulpit
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The Content model fields: `Visibility`, the visibility / draft / timestamp /
|
|
17
|
+
* tombstone spreads, `ownerRef`, the base `content` resource.
|
|
18
|
+
*/
|
|
19
|
+
export * as Content from "./content.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The registry flag vocabulary, `validateRegistry`, and the derived families /
|
|
23
|
+
* branches.
|
|
24
|
+
*/
|
|
25
|
+
export * as Registry from "./registry.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The append-only node / version chain: create, update, restore, delete,
|
|
29
|
+
* getVersion, compare (with the pure `diffStates`), restoreVersion, history —
|
|
30
|
+
* over a store seam.
|
|
31
|
+
*/
|
|
32
|
+
export * as VersionChain from "./version-chain.ts";
|
|
33
|
+
|
|
34
|
+
/** The list-filter vocabulary the read predicates render into, and its in-memory evaluator. */
|
|
35
|
+
export * as Filter from "./filter.ts";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The subtype operation factory: `LeafAccess`, the read scope and viewer
|
|
39
|
+
* predicates, the write gates, `defineSubtype` and its handlers seam.
|
|
40
|
+
*/
|
|
41
|
+
export * as Subtype from "./subtype.ts";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The publication axis, the moderation state, and the cache serveability
|
|
45
|
+
* policy (`isPubliclyServeable`, the `serveable` hook).
|
|
46
|
+
*/
|
|
47
|
+
export * as Publication from "./publication.ts";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The content-addressed document store seam (`DocumentStore`), the
|
|
51
|
+
* `DocumentHash` it is keyed by, and the `documentRef` field spread a version
|
|
52
|
+
* row carries the reference in.
|
|
53
|
+
*/
|
|
54
|
+
export * as Documents from "./documents.ts";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The edge operations factory: a relation between nodes as its own family
|
|
58
|
+
* (`defineEdgeOps` over an `EdgeStore` seam), readable only when every end
|
|
59
|
+
* is, with declared cardinality limits.
|
|
60
|
+
*/
|
|
61
|
+
export * as Edges from "./edges.ts";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Suggested edits: a contributor's patch against a node's version, accepted
|
|
65
|
+
* through the target's own update or rejected (`defineProposals` over a
|
|
66
|
+
* `ProposalStore` seam), with its two-tier stale / conflict classification.
|
|
67
|
+
*/
|
|
68
|
+
export * as Proposals from "./proposals.ts";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* File and media leaves: the blob-store seam (`BlobStore`) keyed by the
|
|
72
|
+
* content hash, the file field spread, the commit-ordered byte write
|
|
73
|
+
* (`defineFileWrites`) and the usage index's edge ends.
|
|
74
|
+
*/
|
|
75
|
+
export * as Blobs from "./blobs.ts";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Routable content: a leaf's path claims tied to its version chain
|
|
79
|
+
* (`routable` over a `PathStore` seam), the aliases a path change leaves
|
|
80
|
+
* behind, and the gated resolution of a path.
|
|
81
|
+
*/
|
|
82
|
+
export * as Paths from "./paths.ts";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The moderation workflow: the transition rules as a before hook
|
|
86
|
+
* (`moderationHook`), the decision (`defineModeration`), the review-queue
|
|
87
|
+
* filter, threshold grading (`gradeScores`) and member reports
|
|
88
|
+
* (`defineReports` over a `ReportStore` seam).
|
|
89
|
+
*/
|
|
90
|
+
export * as Moderation from "./moderation.ts";
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The working copy: an editor's autosave buffer outside the version chain
|
|
94
|
+
* (`WorkingCopyStore`), committed as one ordinary version
|
|
95
|
+
* (`defineWorkingCopy`).
|
|
96
|
+
*/
|
|
97
|
+
export * as WorkingCopy from "./working-copy.ts";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The comments family: threaded comments on any node as versioned owned
|
|
101
|
+
* leaves (`defineComments`), behind the target's read gate and the
|
|
102
|
+
* moderation gate.
|
|
103
|
+
*/
|
|
104
|
+
export * as Comments from "./comments.ts";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Purge: erasure, the one destructive path through a node's history. It
|
|
108
|
+
* hard-deletes a node's chain and the rows kept for it, then the documents
|
|
109
|
+
* and bytes nothing else references (`definePurge` over a `PurgeStore`).
|
|
110
|
+
*/
|
|
111
|
+
export * as Purge from "./purge.ts";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Hybrid search: reciprocal rank fusion over ranked branches, the reranker
|
|
115
|
+
* and branch seams every branch's scope runs through, and the embedding
|
|
116
|
+
* pipeline as seams (`fuseRankings`, `hybridSearch`, `embedPending`).
|
|
117
|
+
*/
|
|
118
|
+
export * as Search from "./search.ts";
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Write budgets: a declared ceiling on an account's writes in a rolling
|
|
122
|
+
* window, spent inside the write as a before hook or by a plain-row family,
|
|
123
|
+
* over a `BudgetCounter` seam (`bindBudget`, `budgetHook`).
|
|
124
|
+
*/
|
|
125
|
+
export * as Budgets from "./budgets.ts";
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reactions: one reaction per account per node, a kind from the family's
|
|
129
|
+
* vocabulary, behind the target's read gate, with a grouped tally
|
|
130
|
+
* (`defineReactions` over a `ReactionStore`).
|
|
131
|
+
*/
|
|
132
|
+
export * as Reactions from "./reactions.ts";
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The reference resource cache: `./subtype`'s `ResourceCacheSeam`, with a
|
|
136
|
+
* read-through `get`, over a minimal versioned key-value seam
|
|
137
|
+
* (`makeResourceCache` over a `VersionedKv`).
|
|
138
|
+
*/
|
|
139
|
+
export * as Cache from "./cache.ts";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ordered collections: positioned members of a parent node, the order kept
|
|
143
|
+
* beside the chain or versioned with the parent (`defineCollection` over a
|
|
144
|
+
* `CollectionStore`).
|
|
145
|
+
*/
|
|
146
|
+
export * as Collections from "./collections.ts";
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The social graph: follow, mute and block between accounts, the follower
|
|
150
|
+
* seam filled from it, and the opt-in read filter over silenced authors
|
|
151
|
+
* (`defineSocial` over a `SocialStore`).
|
|
152
|
+
*/
|
|
153
|
+
export * as Social from "./social.ts";
|