@thomasfosterau/pulpit 0.0.0-stage → 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 +1272 -2
- 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 +145 -3
- 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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The comments family** — threaded comments on any node, as versioned
|
|
3
|
+
* owned leaves, behind the target's own read gate and the moderation gate.
|
|
4
|
+
*
|
|
5
|
+
* The behavioural spec is gardeneria's `apps/api/src/operations/comments.ts`
|
|
6
|
+
* (`isCommentReadableBy`, `moderationVisibilityFilter`,
|
|
7
|
+
* `targetVisibilityFilter`) and its tests
|
|
8
|
+
* (`comment-visibility.test.ts`, `comment-target-visibility-flip.test.ts`);
|
|
9
|
+
* a `#NNN` here names a gardeneria issue.
|
|
10
|
+
*
|
|
11
|
+
* ## The model (owner decisions on pulpit#50)
|
|
12
|
+
*
|
|
13
|
+
* - **A comment is a versioned owned leaf**, built with `./subtype`'s
|
|
14
|
+
* `defineSubtype`: edits append versions, a delete tombstones, and its
|
|
15
|
+
* history is gated like any leaf's. Its moderation state is
|
|
16
|
+
* `./moderation`'s, so the leaf's hooks carry `moderationHook` (a new
|
|
17
|
+
* comment starts `pending`).
|
|
18
|
+
* - **Reply depth is the family's `maxDepth`**, default `1` (gardeneria):
|
|
19
|
+
* `0` is flat (website), `N` threads `N` deep. A reply past it fails
|
|
20
|
+
* {@link CommentDepthExceeded}. Each comment stores its `targetType`,
|
|
21
|
+
* `targetId`, `parentId` (`null` at the top) and `depth` (`0` at the
|
|
22
|
+
* top), written by `create`.
|
|
23
|
+
*
|
|
24
|
+
* ## The read gates (all three, `SEC-AUTH-ENUM-001`)
|
|
25
|
+
*
|
|
26
|
+
* A comment reads to a viewer only when:
|
|
27
|
+
*
|
|
28
|
+
* 1. the comment leaf's own `get` serves it (its owned visibility, and a
|
|
29
|
+
* tombstone only to the view-deleted rung);
|
|
30
|
+
* 2. it is `approved`, or the viewer wrote it, or holds the moderate rung
|
|
31
|
+
* ({@link canViewModerated} / {@link moderationListFilter}, one rule in
|
|
32
|
+
* two readings): moderation is not ownership, so the target's owner
|
|
33
|
+
* does not see a pending comment on their own row;
|
|
34
|
+
* 3. the viewer can read its target, through the target leaf's own `get`:
|
|
35
|
+
* a comment on a target they can't read is missing to them, whatever
|
|
36
|
+
* its own state, and listing by target checks the target first.
|
|
37
|
+
*
|
|
38
|
+
* Anything else is `NodeNotFound`, never `Forbidden`. Writes to an
|
|
39
|
+
* existing comment are the comment leaf's own (its author, or the moderate
|
|
40
|
+
* rung).
|
|
41
|
+
*
|
|
42
|
+
* @copyright Thomas Foster
|
|
43
|
+
* @module pulpit/comments
|
|
44
|
+
* @since 0.1.0
|
|
45
|
+
*/
|
|
46
|
+
import * as Effect from "effect/Effect";
|
|
47
|
+
import * as Option from "effect/Option";
|
|
48
|
+
import * as Schema from "effect/Schema";
|
|
49
|
+
import { type ListFilter } from "./filter.ts";
|
|
50
|
+
import type { ModerationDeclaration } from "./moderation.ts";
|
|
51
|
+
import { type AccessPolicy, type Forbidden, type InvalidListOrder, type LeafRow, type LeafStore, type ListOrder, type Page, type SubtypeOps, type Unauthorized, type Viewer, type WriteRejected } from "./subtype.ts";
|
|
52
|
+
import { NodeNotFound, type ChainState, type ChainWrite, type NodeAlreadyExists, type VersionConflict } from "./version-chain.ts";
|
|
53
|
+
declare const CommentDepthExceeded_base: Schema.Class<CommentDepthExceeded, Schema.TaggedStruct<"CommentDepthExceeded", {
|
|
54
|
+
readonly type: Schema.String;
|
|
55
|
+
readonly parentId: Schema.String;
|
|
56
|
+
readonly maxDepth: Schema.Finite;
|
|
57
|
+
}>, import("effect/Cause").YieldableError & {
|
|
58
|
+
readonly brand: unique symbol;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* A reply that would nest deeper than the family's `maxDepth`: the wire's
|
|
62
|
+
* 422.
|
|
63
|
+
*
|
|
64
|
+
* @since 0.1.0
|
|
65
|
+
*/
|
|
66
|
+
export declare class CommentDepthExceeded extends CommentDepthExceeded_base {
|
|
67
|
+
}
|
|
68
|
+
declare const CommentDeclarationError_base: Schema.Class<CommentDeclarationError, Schema.TaggedStruct<"CommentDeclarationError", {
|
|
69
|
+
readonly message: Schema.String;
|
|
70
|
+
}>, import("effect/Cause").YieldableError & {
|
|
71
|
+
readonly brand: unique symbol;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* A comments family declared against the rules: a `maxDepth` that is not
|
|
75
|
+
* a non-negative safe integer. Thrown by {@link defineComments} when the
|
|
76
|
+
* family is declared, never an Effect failure.
|
|
77
|
+
*
|
|
78
|
+
* @since 0.1.0
|
|
79
|
+
*/
|
|
80
|
+
export declare class CommentDeclarationError extends CommentDeclarationError_base {
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The columns the moderation gate reads: the comment's author (its owner
|
|
84
|
+
* column) and its moderation state.
|
|
85
|
+
*
|
|
86
|
+
* @since 0.1.0
|
|
87
|
+
*/
|
|
88
|
+
export interface ModeratedColumns {
|
|
89
|
+
readonly authorColumn: string;
|
|
90
|
+
readonly stateColumn: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The moderation gate on one comment row: the moderate rung reads every
|
|
94
|
+
* state, a signed-in viewer their own comments in every state, and
|
|
95
|
+
* everyone else only an exactly `approved` one. The single-row reading of
|
|
96
|
+
* {@link moderationListFilter}; the two never disagree. Never fails.
|
|
97
|
+
*
|
|
98
|
+
* @since 0.1.0
|
|
99
|
+
*/
|
|
100
|
+
export declare const canViewModerated: (viewer: Viewer, policy: AccessPolicy, columns: ModeratedColumns, row: LeafRow) => boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The moderation gate as a list filter: {@link canViewModerated}'s three
|
|
103
|
+
* arms.
|
|
104
|
+
*
|
|
105
|
+
* @since 0.1.0
|
|
106
|
+
*/
|
|
107
|
+
export declare const moderationListFilter: <Following>(viewer: Viewer, policy: AccessPolicy, columns: ModeratedColumns) => ListFilter<Following>;
|
|
108
|
+
/**
|
|
109
|
+
* A target leaf a comment may hang off: its read, which the target gate
|
|
110
|
+
* runs as the viewer.
|
|
111
|
+
*
|
|
112
|
+
* @since 0.1.0
|
|
113
|
+
*/
|
|
114
|
+
export interface CommentTarget<E, R> {
|
|
115
|
+
/** The target leaf's own `get`: `NodeNotFound` for a target the viewer can't read. */
|
|
116
|
+
readonly get: (id: string) => Effect.Effect<unknown, NodeNotFound | E, R>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A comments family's declaration.
|
|
120
|
+
*
|
|
121
|
+
* @since 0.1.0
|
|
122
|
+
*/
|
|
123
|
+
export interface CommentsDefinition<Resource, Following, E, R> {
|
|
124
|
+
/**
|
|
125
|
+
* The comment leaf's operations (`defineSubtype` over an owned access,
|
|
126
|
+
* with `moderationHook` among its hooks).
|
|
127
|
+
*/
|
|
128
|
+
readonly comments: SubtypeOps<Resource, E, R>;
|
|
129
|
+
/** The comment leaf's current-version view: the rows the gates read their facts from. */
|
|
130
|
+
readonly leaves: LeafStore<LeafRow, Following, E, R>;
|
|
131
|
+
/** The leaves a comment may target, by wire type; any other type is `NodeNotFound`. */
|
|
132
|
+
readonly targets: {
|
|
133
|
+
readonly [type: string]: CommentTarget<E, R>;
|
|
134
|
+
};
|
|
135
|
+
readonly viewer: Effect.Effect<Viewer, never, R>;
|
|
136
|
+
readonly policy: AccessPolicy;
|
|
137
|
+
/** The moderation declaration the comment leaf's hook uses; its `column` is the state the gate reads. */
|
|
138
|
+
readonly moderation: ModerationDeclaration;
|
|
139
|
+
/** The comment's author column: the leaf's `ownerColumn` (conventionally `ownerId`). */
|
|
140
|
+
readonly authorColumn: string;
|
|
141
|
+
/**
|
|
142
|
+
* How deep replies nest: `0` flat, `1` (the default) one level of
|
|
143
|
+
* replies, `N` deeper. A non-negative safe integer, or
|
|
144
|
+
* {@link defineComments} throws {@link CommentDeclarationError}.
|
|
145
|
+
*/
|
|
146
|
+
readonly maxDepth?: number | undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A comment create: its target, its parent comment (none for a top-level
|
|
150
|
+
* comment), and the leaf's own columns (its body, its visibility).
|
|
151
|
+
*
|
|
152
|
+
* @since 0.1.0
|
|
153
|
+
*/
|
|
154
|
+
export interface CommentCreate {
|
|
155
|
+
readonly id: Option.Option<string>;
|
|
156
|
+
readonly targetType: string;
|
|
157
|
+
readonly targetId: string;
|
|
158
|
+
readonly parentId: Option.Option<string>;
|
|
159
|
+
readonly state: ChainState;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* One page of a target's comments.
|
|
163
|
+
*
|
|
164
|
+
* @since 0.1.0
|
|
165
|
+
*/
|
|
166
|
+
export interface CommentListRequest {
|
|
167
|
+
readonly targetType: string;
|
|
168
|
+
readonly targetId: string;
|
|
169
|
+
/**
|
|
170
|
+
* None: every comment on the target; some none: the top-level comments
|
|
171
|
+
* (`depth = 0`); some parent: that comment's replies.
|
|
172
|
+
*/
|
|
173
|
+
readonly parentId: Option.Option<Option.Option<string>>;
|
|
174
|
+
readonly limit: number;
|
|
175
|
+
readonly offset: number;
|
|
176
|
+
/** Tombstoned comments too, for the view-deleted rung (as the leaf's own `list` takes it). */
|
|
177
|
+
readonly includeDeleted: boolean;
|
|
178
|
+
readonly orderBy?: ReadonlyArray<ListOrder> | undefined;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The operations {@link defineComments} produces.
|
|
182
|
+
*
|
|
183
|
+
* @since 0.1.0
|
|
184
|
+
*/
|
|
185
|
+
export interface CommentOps<Resource, E, R> {
|
|
186
|
+
/**
|
|
187
|
+
* Comment on a target the viewer can read, as a reply to `parentId`
|
|
188
|
+
* when given (a comment on the same target the viewer can read). Fails
|
|
189
|
+
* `NodeNotFound` for an unreadable target or parent, a parent on another
|
|
190
|
+
* target included; {@link CommentDepthExceeded} past `maxDepth`; and as
|
|
191
|
+
* the comment leaf's `create` does (`Unauthorized` when anonymous).
|
|
192
|
+
*/
|
|
193
|
+
readonly create: (input: CommentCreate) => Effect.Effect<ChainWrite, NodeNotFound | CommentDepthExceeded | Unauthorized | Forbidden | WriteRejected | NodeAlreadyExists | VersionConflict | E, R>;
|
|
194
|
+
/** One comment, through all three read gates; `NodeNotFound` otherwise. */
|
|
195
|
+
readonly get: (id: string) => Effect.Effect<Resource, NodeNotFound | E, R>;
|
|
196
|
+
/**
|
|
197
|
+
* A target's comments, after its target gate (`NodeNotFound` for a target
|
|
198
|
+
* the viewer can't read), through the comment leaf's own list and the
|
|
199
|
+
* moderation gate. The page is clamped to `MAX_PAGE_LIMIT`; an unsortable
|
|
200
|
+
* order fails `InvalidListOrder`, as the comment leaf's `list` does.
|
|
201
|
+
*/
|
|
202
|
+
readonly listByTarget: (request: CommentListRequest) => Effect.Effect<Page<Resource>, NodeNotFound | InvalidListOrder | E, R>;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Bind a comments family over its comment leaf. Edits, deletes and history
|
|
206
|
+
* stay the comment leaf's own operations.
|
|
207
|
+
*
|
|
208
|
+
* @since 0.1.0
|
|
209
|
+
*/
|
|
210
|
+
export declare const defineComments: <Resource, Following, E, R>(definition: CommentsDefinition<Resource, Following, E, R>) => CommentOps<Resource, E, R>;
|
|
211
|
+
export {};
|
|
212
|
+
//# sourceMappingURL=comments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"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"}
|
package/dist/comments.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The comments family** — threaded comments on any node, as versioned
|
|
3
|
+
* owned leaves, behind the target's own read gate and the moderation gate.
|
|
4
|
+
*
|
|
5
|
+
* The behavioural spec is gardeneria's `apps/api/src/operations/comments.ts`
|
|
6
|
+
* (`isCommentReadableBy`, `moderationVisibilityFilter`,
|
|
7
|
+
* `targetVisibilityFilter`) and its tests
|
|
8
|
+
* (`comment-visibility.test.ts`, `comment-target-visibility-flip.test.ts`);
|
|
9
|
+
* a `#NNN` here names a gardeneria issue.
|
|
10
|
+
*
|
|
11
|
+
* ## The model (owner decisions on pulpit#50)
|
|
12
|
+
*
|
|
13
|
+
* - **A comment is a versioned owned leaf**, built with `./subtype`'s
|
|
14
|
+
* `defineSubtype`: edits append versions, a delete tombstones, and its
|
|
15
|
+
* history is gated like any leaf's. Its moderation state is
|
|
16
|
+
* `./moderation`'s, so the leaf's hooks carry `moderationHook` (a new
|
|
17
|
+
* comment starts `pending`).
|
|
18
|
+
* - **Reply depth is the family's `maxDepth`**, default `1` (gardeneria):
|
|
19
|
+
* `0` is flat (website), `N` threads `N` deep. A reply past it fails
|
|
20
|
+
* {@link CommentDepthExceeded}. Each comment stores its `targetType`,
|
|
21
|
+
* `targetId`, `parentId` (`null` at the top) and `depth` (`0` at the
|
|
22
|
+
* top), written by `create`.
|
|
23
|
+
*
|
|
24
|
+
* ## The read gates (all three, `SEC-AUTH-ENUM-001`)
|
|
25
|
+
*
|
|
26
|
+
* A comment reads to a viewer only when:
|
|
27
|
+
*
|
|
28
|
+
* 1. the comment leaf's own `get` serves it (its owned visibility, and a
|
|
29
|
+
* tombstone only to the view-deleted rung);
|
|
30
|
+
* 2. it is `approved`, or the viewer wrote it, or holds the moderate rung
|
|
31
|
+
* ({@link canViewModerated} / {@link moderationListFilter}, one rule in
|
|
32
|
+
* two readings): moderation is not ownership, so the target's owner
|
|
33
|
+
* does not see a pending comment on their own row;
|
|
34
|
+
* 3. the viewer can read its target, through the target leaf's own `get`:
|
|
35
|
+
* a comment on a target they can't read is missing to them, whatever
|
|
36
|
+
* its own state, and listing by target checks the target first.
|
|
37
|
+
*
|
|
38
|
+
* Anything else is `NodeNotFound`, never `Forbidden`. Writes to an
|
|
39
|
+
* existing comment are the comment leaf's own (its author, or the moderate
|
|
40
|
+
* rung).
|
|
41
|
+
*
|
|
42
|
+
* @copyright Thomas Foster
|
|
43
|
+
* @module pulpit/comments
|
|
44
|
+
* @since 0.1.0
|
|
45
|
+
*/
|
|
46
|
+
import * as Effect from "effect/Effect";
|
|
47
|
+
import * as Option from "effect/Option";
|
|
48
|
+
import * as Predicate from "effect/Predicate";
|
|
49
|
+
import * as Schema from "effect/Schema";
|
|
50
|
+
import { always, and, eq, or } from "./filter.js";
|
|
51
|
+
import { MAX_PAGE_LIMIT, } from "./subtype.js";
|
|
52
|
+
import { NodeNotFound, } from "./version-chain.js";
|
|
53
|
+
// ── Errors ────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* A reply that would nest deeper than the family's `maxDepth`: the wire's
|
|
56
|
+
* 422.
|
|
57
|
+
*
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
*/
|
|
60
|
+
export class CommentDepthExceeded extends Schema.TaggedError()("CommentDepthExceeded", {
|
|
61
|
+
type: Schema.String,
|
|
62
|
+
parentId: Schema.String,
|
|
63
|
+
maxDepth: Schema.Finite,
|
|
64
|
+
}) {
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A comments family declared against the rules: a `maxDepth` that is not
|
|
68
|
+
* a non-negative safe integer. Thrown by {@link defineComments} when the
|
|
69
|
+
* family is declared, never an Effect failure.
|
|
70
|
+
*
|
|
71
|
+
* @since 0.1.0
|
|
72
|
+
*/
|
|
73
|
+
export class CommentDeclarationError extends Schema.TaggedError()("CommentDeclarationError", { message: Schema.String }) {
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The moderation gate on one comment row: the moderate rung reads every
|
|
77
|
+
* state, a signed-in viewer their own comments in every state, and
|
|
78
|
+
* everyone else only an exactly `approved` one. The single-row reading of
|
|
79
|
+
* {@link moderationListFilter}; the two never disagree. Never fails.
|
|
80
|
+
*
|
|
81
|
+
* @since 0.1.0
|
|
82
|
+
*/
|
|
83
|
+
export const canViewModerated = (viewer, policy, columns, row) => row[columns.stateColumn] === "approved" ||
|
|
84
|
+
Option.exists(viewer, (account) => policy.canModerate(account) ||
|
|
85
|
+
(Predicate.isString(row[columns.authorColumn]) && row[columns.authorColumn] === account.id));
|
|
86
|
+
/**
|
|
87
|
+
* The moderation gate as a list filter: {@link canViewModerated}'s three
|
|
88
|
+
* arms.
|
|
89
|
+
*
|
|
90
|
+
* @since 0.1.0
|
|
91
|
+
*/
|
|
92
|
+
export const moderationListFilter = (viewer, policy, columns) => Option.match(viewer, {
|
|
93
|
+
onNone: () => eq(columns.stateColumn, "approved"),
|
|
94
|
+
onSome: (account) => policy.canModerate(account)
|
|
95
|
+
? always
|
|
96
|
+
: or(eq(columns.stateColumn, "approved"), eq(columns.authorColumn, account.id)),
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* Bind a comments family over its comment leaf. Edits, deletes and history
|
|
100
|
+
* stay the comment leaf's own operations.
|
|
101
|
+
*
|
|
102
|
+
* @since 0.1.0
|
|
103
|
+
*/
|
|
104
|
+
export const defineComments = (definition) => {
|
|
105
|
+
const { comments, leaves, policy } = definition;
|
|
106
|
+
const type = comments.type;
|
|
107
|
+
const maxDepth = definition.maxDepth ?? 1;
|
|
108
|
+
if (!Number.isSafeInteger(maxDepth) || maxDepth < 0) {
|
|
109
|
+
throw new CommentDeclarationError({
|
|
110
|
+
message: `${type}: maxDepth must be a non-negative safe integer, not ${maxDepth}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const columns = {
|
|
114
|
+
authorColumn: definition.authorColumn,
|
|
115
|
+
stateColumn: definition.moderation.column,
|
|
116
|
+
};
|
|
117
|
+
/** The target gate: the target leaf's own read, as the viewer. */
|
|
118
|
+
const targetReadable = (targetType, targetId) => Option.match(Option.fromUndefinedOr(definition.targets[targetType]), {
|
|
119
|
+
onNone: () => Effect.fail(new NodeNotFound({ type: targetType, id: targetId })),
|
|
120
|
+
onSome: (target) => Effect.asVoid(target.get(targetId)),
|
|
121
|
+
});
|
|
122
|
+
/** The comment row a viewer may read, through all three gates, or `NodeNotFound`. */
|
|
123
|
+
const readableRow = (viewer, id) => Effect.gen(function* () {
|
|
124
|
+
const resource = yield* comments.get(id);
|
|
125
|
+
const row = yield* leaves.get(id);
|
|
126
|
+
if (Option.isNone(row) || !canViewModerated(viewer, policy, columns, row.value)) {
|
|
127
|
+
return yield* new NodeNotFound({ type, id });
|
|
128
|
+
}
|
|
129
|
+
const targetType = row.value["targetType"];
|
|
130
|
+
const targetId = row.value["targetId"];
|
|
131
|
+
if (!Predicate.isString(targetType) || !Predicate.isString(targetId)) {
|
|
132
|
+
return yield* new NodeNotFound({ type, id });
|
|
133
|
+
}
|
|
134
|
+
// A comment on a target the viewer can't read is missing to them.
|
|
135
|
+
yield* Effect.mapError(targetReadable(targetType, targetId), () => new NodeNotFound({ type, id }));
|
|
136
|
+
return { resource, row: row.value };
|
|
137
|
+
});
|
|
138
|
+
/** A row's stored depth: `0` for a top-level comment; a value that is no number reads as past every limit. */
|
|
139
|
+
const depthOf = (row) => {
|
|
140
|
+
const depth = row["depth"];
|
|
141
|
+
return Predicate.isNumber(depth) ? depth : Number.POSITIVE_INFINITY;
|
|
142
|
+
};
|
|
143
|
+
const create = Effect.fn(`${type}.comments.create`)(function* (input) {
|
|
144
|
+
const viewer = yield* definition.viewer;
|
|
145
|
+
yield* targetReadable(input.targetType, input.targetId);
|
|
146
|
+
let depth = 0;
|
|
147
|
+
if (Option.isSome(input.parentId)) {
|
|
148
|
+
const parentId = input.parentId.value;
|
|
149
|
+
const parent = yield* readableRow(viewer, parentId);
|
|
150
|
+
if (parent.row["targetId"] !== input.targetId ||
|
|
151
|
+
parent.row["targetType"] !== input.targetType) {
|
|
152
|
+
return yield* new NodeNotFound({ type, id: parentId });
|
|
153
|
+
}
|
|
154
|
+
depth = depthOf(parent.row) + 1;
|
|
155
|
+
if (depth > maxDepth)
|
|
156
|
+
return yield* new CommentDepthExceeded({ type, parentId, maxDepth });
|
|
157
|
+
}
|
|
158
|
+
return yield* comments.create({
|
|
159
|
+
id: input.id,
|
|
160
|
+
state: {
|
|
161
|
+
...input.state,
|
|
162
|
+
targetType: input.targetType,
|
|
163
|
+
targetId: input.targetId,
|
|
164
|
+
parentId: Option.getOrNull(input.parentId),
|
|
165
|
+
depth,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
const get = Effect.fn(`${type}.comments.get`)(function* (id) {
|
|
170
|
+
return (yield* readableRow(yield* definition.viewer, id)).resource;
|
|
171
|
+
});
|
|
172
|
+
const listByTarget = Effect.fn(`${type}.comments.listByTarget`)(function* (request) {
|
|
173
|
+
const viewer = yield* definition.viewer;
|
|
174
|
+
yield* targetReadable(request.targetType, request.targetId);
|
|
175
|
+
const parent = Option.match(request.parentId, {
|
|
176
|
+
onNone: () => always,
|
|
177
|
+
onSome: (parentId) => Option.match(parentId, {
|
|
178
|
+
onNone: () => eq("depth", 0),
|
|
179
|
+
onSome: (id) => eq("parentId", id),
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
return yield* comments.list({
|
|
183
|
+
where: and(eq("targetType", request.targetType), eq("targetId", request.targetId), moderationListFilter(viewer, policy, columns), parent),
|
|
184
|
+
includeDeleted: request.includeDeleted,
|
|
185
|
+
limit: Math.min(request.limit, MAX_PAGE_LIMIT),
|
|
186
|
+
offset: request.offset,
|
|
187
|
+
orderBy: request.orderBy,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
return { create, get, listByTarget };
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.js","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The Content model half of the kernel** — the shared Content-axis field
|
|
3
|
+
* spreads and the base `content` resource, defined via
|
|
4
|
+
* `@thomasfosterau/effect-jsonapi`'s `Resource.make`.
|
|
5
|
+
*
|
|
6
|
+
* Ported from gardeneria's `packages/models/src/models/content.ts` (the
|
|
7
|
+
* three-state `Visibility`; website's copy is the older two-state one) and the
|
|
8
|
+
* timestamp / tombstone spreads of its `packages/models/src/utilities.ts`. A
|
|
9
|
+
* `#NNN` here names a gardeneria issue.
|
|
10
|
+
*
|
|
11
|
+
* Every spread is a plain `const` field map a Content subtype spreads into its
|
|
12
|
+
* own `Resource.make({ attributes })`, so one declaration drives the resource
|
|
13
|
+
* object, its documents and the create / update projections (the tri-state
|
|
14
|
+
* rule: a value sets, `null` clears a nullable column, `undefined` carries
|
|
15
|
+
* forward — declared on the descriptor, never re-spelled at a call site).
|
|
16
|
+
*
|
|
17
|
+
* @copyright Thomas Foster
|
|
18
|
+
* @module pulpit/content
|
|
19
|
+
* @since 0.1.0
|
|
20
|
+
*/
|
|
21
|
+
import { Relationship, Resource } from "@thomasfosterau/effect-jsonapi";
|
|
22
|
+
import * as Schema from "effect/Schema";
|
|
23
|
+
/**
|
|
24
|
+
* Visibility — the explicit access axis the publication policy reads,
|
|
25
|
+
* independent of soft-delete and the draft flag. Three states, because the
|
|
26
|
+
* kernel is multi-tenant:
|
|
27
|
+
*
|
|
28
|
+
* - `public` — anyone, signed in or not; the only state the KV read-through
|
|
29
|
+
* cache may ever hold (`isPubliclyServeable` in `./publication`);
|
|
30
|
+
* - `followers` — the owner, the owner's followers (the follower seam in
|
|
31
|
+
* `./subtype`), and the privileged rung;
|
|
32
|
+
* - `private` — the owner and the privileged rung only.
|
|
33
|
+
*
|
|
34
|
+
* The predicate lives in ONE place — `canViewOwned` in `./subtype` — and
|
|
35
|
+
* every read of an owned row goes through it. Content carries it on the
|
|
36
|
+
* Content branch's version row; owned Entity / File leaves carry the same
|
|
37
|
+
* column on their own version row, declared with
|
|
38
|
+
* {@link ownedVisibilityResourceFields}.
|
|
39
|
+
*
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
*/
|
|
42
|
+
export declare const Visibility: Schema.Literals<readonly ["public", "followers", "private"]>;
|
|
43
|
+
/**
|
|
44
|
+
* The {@link Visibility} literal union: `"public" | "followers" | "private"`.
|
|
45
|
+
*
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
export type Visibility = typeof Visibility.Type;
|
|
49
|
+
/**
|
|
50
|
+
* The two orthogonal Content-branch axes every Content subtype carries on its
|
|
51
|
+
* version row: `visibility` (three-state, above) and the binary `draft` flag
|
|
52
|
+
* (`PUB-009` / `PUB-010`). Both are required on the resource but optional to
|
|
53
|
+
* supply at create / update (a DB default applies, updates carry forward);
|
|
54
|
+
* neither is ever cleared.
|
|
55
|
+
*
|
|
56
|
+
* `Resource.attribute(schema, { create: "optional" })` is the
|
|
57
|
+
* required-on-resource, optional-at-both-inputs, never-cleared descriptor.
|
|
58
|
+
*
|
|
59
|
+
* @since 0.1.0
|
|
60
|
+
*/
|
|
61
|
+
export declare const contentVisibilityResourceFields: {
|
|
62
|
+
readonly visibility: Resource.Attribute<Schema.Literals<readonly ["public", "followers", "private"]>, true, "optional", "optional", false>;
|
|
63
|
+
readonly draft: Resource.Attribute<Schema.Boolean, true, "optional", "optional", false>;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* The visibility / draft fields as **create-input** shapes (`Schema.optional`,
|
|
67
|
+
* non-null), for Content subtypes' hand-written `Create` structs — a form
|
|
68
|
+
* adapter passes `value | undefined`, which `createInput`'s strict
|
|
69
|
+
* `optionalKey` rejects, so the create struct is hand-written.
|
|
70
|
+
*
|
|
71
|
+
* @since 0.1.0
|
|
72
|
+
*/
|
|
73
|
+
export declare const contentVisibilityCreateFields: {
|
|
74
|
+
readonly visibility: Schema.optional<Schema.Literals<readonly ["public", "followers", "private"]>>;
|
|
75
|
+
readonly draft: Schema.optional<Schema.Boolean>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The visibility axis alone, for **owned Entity / File leaves** (a garden, an
|
|
79
|
+
* organism, an image): they carry `visibility` on their own version row but
|
|
80
|
+
* no `draft` flag — a garden is not a draft of anything. Same descriptor as
|
|
81
|
+
* the Content pair: required on the resource, optional at both inputs, never
|
|
82
|
+
* cleared.
|
|
83
|
+
*
|
|
84
|
+
* @since 0.1.0
|
|
85
|
+
*/
|
|
86
|
+
export declare const ownedVisibilityResourceFields: {
|
|
87
|
+
readonly visibility: Resource.Attribute<Schema.Literals<readonly ["public", "followers", "private"]>, true, "optional", "optional", false>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The version-chain timestamps every node-family resource carries:
|
|
91
|
+
* `createdAt` (the node's version-0 commit time) and `updatedAt` (its current
|
|
92
|
+
* version's), server-computed and present on the resource only
|
|
93
|
+
* (`readOnlyAttribute`), never accepted as command input.
|
|
94
|
+
*
|
|
95
|
+
* @since 0.1.0
|
|
96
|
+
*/
|
|
97
|
+
export declare const nodeTimestampResourceFields: {
|
|
98
|
+
readonly createdAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
99
|
+
readonly updatedAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The soft-delete tombstone: `deletedAt` is present only on a tombstoned row
|
|
103
|
+
* (`resource: "optional"` — emitters omit the key entirely while live, #691)
|
|
104
|
+
* and is in no write projection. A tombstone is written by the chain's
|
|
105
|
+
* `delete` operation (`./version-chain`), never by an input.
|
|
106
|
+
*
|
|
107
|
+
* @since 0.1.0
|
|
108
|
+
*/
|
|
109
|
+
export declare const tombstoneResourceFields: {
|
|
110
|
+
readonly deletedAt: Resource.Attribute<Schema.Date, "optional", false, false, false>;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* The attribute spread every **reference-data** leaf carries: the two
|
|
114
|
+
* timestamps and the tombstone. Reference data has no visibility axis of its
|
|
115
|
+
* own — the corpus is publicly readable; a Content-branch reference leaf reads
|
|
116
|
+
* `visibility` off the branch row like every other Content subtype, and shows
|
|
117
|
+
* only a `public` row below the view-private rung.
|
|
118
|
+
*
|
|
119
|
+
* @since 0.1.0
|
|
120
|
+
*/
|
|
121
|
+
export declare const referenceLeafResourceFields: {
|
|
122
|
+
readonly createdAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
123
|
+
readonly updatedAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
124
|
+
readonly deletedAt: Resource.Attribute<Schema.Date, "optional", false, false, false>;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The attribute spread every **owned Content leaf** opens its attributes with:
|
|
128
|
+
* the timestamps, the tombstone, and the access axis —
|
|
129
|
+
* {@link referenceLeafResourceFields} plus {@link ownedVisibilityResourceFields}.
|
|
130
|
+
*
|
|
131
|
+
* `draft` is deliberately absent from THIS shared spread. The family that
|
|
132
|
+
* actually wants a bare boolean "come back and finish this later" flag
|
|
133
|
+
* declares it directly through {@link contentVisibilityResourceFields};
|
|
134
|
+
* folding it in here would put an unused, always-`false` `draft` attribute on
|
|
135
|
+
* every other owned Content leaf's wire contract. A leaf gated by a
|
|
136
|
+
* publication *stamp* (`publishedAt`, the axis `./publication` reads) carries
|
|
137
|
+
* that on its own row instead — a different axis from `draft`'s hard gate
|
|
138
|
+
* (#1118).
|
|
139
|
+
*
|
|
140
|
+
* @since 0.1.0
|
|
141
|
+
*/
|
|
142
|
+
export declare const ownedContentLeafResourceFields: {
|
|
143
|
+
readonly visibility: Resource.Attribute<Schema.Literals<readonly ["public", "followers", "private"]>, true, "optional", "optional", false>;
|
|
144
|
+
readonly createdAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
145
|
+
readonly updatedAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
146
|
+
readonly deletedAt: Resource.Attribute<Schema.Date, "optional", false, false, false>;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The `owner` relationship every **owned** leaf carries — the account family
|
|
150
|
+
* the application supplies (its `users` resource) that owns the row.
|
|
151
|
+
*
|
|
152
|
+
* Server-assigned from the authenticated caller at creation and never
|
|
153
|
+
* re-parented: the operation layer stamps the owner from the viewer rather
|
|
154
|
+
* than from the payload (`stampOwner` in `./subtype`), so an application's
|
|
155
|
+
* hand-written create / update structs omit it. Spread into a leaf's
|
|
156
|
+
* `Resource.make({ relationships })` to opt in — the registry flags the same
|
|
157
|
+
* leaf `owned`, and the subtype factory's `access: { kind: "owned", … }`
|
|
158
|
+
* names the column the linkage reads.
|
|
159
|
+
*
|
|
160
|
+
* `Relationship.one`: every owned row has an owner. The target may still fail
|
|
161
|
+
* to resolve (deleting an account does not cascade to its rows); consumers
|
|
162
|
+
* degrade to a neutral label rather than erroring.
|
|
163
|
+
*
|
|
164
|
+
* Note that effect-jsonapi lists a `one` relationship as **required** in
|
|
165
|
+
* the generated `createPayload`; an application serving the generated payload
|
|
166
|
+
* strips the linkage at its handler seam. effect-jsonapi has no read-only
|
|
167
|
+
* linkage descriptor (effect-jsonapi#172 closed without one; 0.17.0 has
|
|
168
|
+
* none), and `tests/content.test.ts` pins the required linkage so the day one
|
|
169
|
+
* ships is visible.
|
|
170
|
+
*
|
|
171
|
+
* @since 0.1.0
|
|
172
|
+
*/
|
|
173
|
+
export declare const ownerRef: <Owner extends Resource.Any>(owner: () => Owner) => {
|
|
174
|
+
owner: Relationship.One<Owner, false, undefined, NoInfer<Schema.$Record<Schema.String, Schema.Unknown>>>;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* The base Content resource's attributes: the timestamps and the tombstone —
|
|
178
|
+
* exactly {@link referenceLeafResourceFields}, named for the resource that
|
|
179
|
+
* carries them so a subtype extending the base and a leaf spreading the
|
|
180
|
+
* fields declare the same wire shape.
|
|
181
|
+
*
|
|
182
|
+
* @since 0.1.0
|
|
183
|
+
*/
|
|
184
|
+
export declare const contentResourceAttributeFields: {
|
|
185
|
+
readonly createdAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
186
|
+
readonly updatedAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
187
|
+
readonly deletedAt: Resource.Attribute<Schema.Date, "optional", false, false, false>;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Build the base `content` resource with the application's own id brand
|
|
191
|
+
* (`makeContentResource(ContentId)`) — the shape {@link ContentResource}
|
|
192
|
+
* carries with the auto-derived `Resource.Id<"content">`. An application whose
|
|
193
|
+
* ids are branded hierarchically (a `PostId` that is also a `ContentId` and a
|
|
194
|
+
* `NodeId`) declares the base with its own brand so the two agree.
|
|
195
|
+
*
|
|
196
|
+
* @since 0.1.0
|
|
197
|
+
*/
|
|
198
|
+
export declare const makeContentResource: <IdSchema extends Schema.Codec<any, string>>(id: IdSchema) => Resource.Resource<"content", {
|
|
199
|
+
readonly createdAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
200
|
+
readonly updatedAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
201
|
+
readonly deletedAt: Resource.Attribute<Schema.Date, "optional", false, false, false>;
|
|
202
|
+
}, Record<never, never>, Schema.$Record<Schema.String, Schema.Unknown>, IdSchema>;
|
|
203
|
+
/**
|
|
204
|
+
* The base Content resource with the auto-derived id (timestamps + soft-delete tombstone).
|
|
205
|
+
*
|
|
206
|
+
* @since 0.1.0
|
|
207
|
+
*/
|
|
208
|
+
export declare const ContentResource: Resource.Resource<"content", {
|
|
209
|
+
readonly createdAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
210
|
+
readonly updatedAt: Resource.ReadOnlyAttribute<Schema.Date>;
|
|
211
|
+
readonly deletedAt: Resource.Attribute<Schema.Date, "optional", false, false, false>;
|
|
212
|
+
}, Record<never, never>, Schema.$Record<Schema.String, Schema.Unknown>, Resource.Id<"content">>;
|
|
213
|
+
/**
|
|
214
|
+
* Inferred type for a base content resource.
|
|
215
|
+
*
|
|
216
|
+
* @since 0.1.0
|
|
217
|
+
*/
|
|
218
|
+
export type Content = typeof ContentResource.Type;
|
|
219
|
+
/**
|
|
220
|
+
* Lightweight `{ type, id }` reference to content.
|
|
221
|
+
*
|
|
222
|
+
* @since 0.1.0
|
|
223
|
+
*/
|
|
224
|
+
export declare const ContentRef: Resource.Identifier<"content", Resource.Id<"content">>;
|
|
225
|
+
/**
|
|
226
|
+
* Inferred type for a content reference.
|
|
227
|
+
*
|
|
228
|
+
* @since 0.1.0
|
|
229
|
+
*/
|
|
230
|
+
export type ContentRef = typeof ContentRef.Type;
|
|
231
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,8DAAsD,CAAC;AAC9E;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,+BAA+B;aAC1C,UAAU;aACV,KAAK;CACG,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B;aACxC,UAAU;aACV,KAAK;CACG,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B;aACxC,UAAU;CACF,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B;aACtC,SAAS;aACT,SAAS;CACD,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB;aAClC,SAAS;CAKD,CAAC;AAEX;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B;;;;CAG9B,CAAC;AAEX;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,8BAA8B;;;;;CAGjC,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,SAAS,QAAQ,CAAC,GAAG,SAAS,MAAM,KAAK;IACrE,KAAK;CACL,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B;;;;CAA8B,CAAC;AAE1E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,SAAS,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,QAAQ;;;;iFACd,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;+FAE1B,CAAC;AAEH;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,eAAe,CAAC,IAAI,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,UAAU,wDAA6B,CAAC;AAErD;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAC"}
|