@peerbit/document 13.0.43 → 13.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.
@@ -0,0 +1,277 @@
1
+ import type { PublicSignKey } from "@peerbit/crypto";
2
+ import type { Context } from "@peerbit/document-interface";
3
+
4
+ export type DocumentTransformFacts = {
5
+ entryPublicKeys?: readonly PublicSignKey[];
6
+ };
7
+
8
+ export type DocumentTransformer<T, I> = (
9
+ obj: T,
10
+ context: Context,
11
+ facts?: DocumentTransformFacts,
12
+ ) => I | Promise<I>;
13
+
14
+ type DocumentTransformSourceDescriptor =
15
+ | {
16
+ readonly kind: "field";
17
+ readonly path: string | readonly string[];
18
+ }
19
+ | {
20
+ readonly kind: "context";
21
+ readonly field: "created" | "modified" | "head" | "gid" | "size";
22
+ }
23
+ | {
24
+ readonly kind: "entryFirstSignerPublicKey";
25
+ };
26
+
27
+ export type DocumentTransformDescriptor =
28
+ | {
29
+ readonly kind: "identity";
30
+ }
31
+ | {
32
+ readonly kind: "pick";
33
+ readonly fields: readonly (string | readonly string[])[];
34
+ }
35
+ | {
36
+ readonly kind: "project";
37
+ readonly fields: readonly {
38
+ readonly target: string | readonly string[];
39
+ readonly source: DocumentTransformSourceDescriptor;
40
+ }[];
41
+ };
42
+
43
+ const NATIVE_DOCUMENT_TRANSFORM = Symbol.for(
44
+ "@peerbit/document/native-document-transform",
45
+ );
46
+
47
+ type DescribedDocumentTransformer<T, I> = DocumentTransformer<T, I> & {
48
+ readonly [NATIVE_DOCUMENT_TRANSFORM]?: DocumentTransformDescriptor;
49
+ };
50
+
51
+ type DocumentTransformSource =
52
+ | string
53
+ | readonly string[]
54
+ | DocumentTransformSourceDescriptor;
55
+
56
+ const copyBytes = (bytes: Uint8Array): Uint8Array => new Uint8Array(bytes);
57
+
58
+ const freezePath = (
59
+ path: string | readonly string[],
60
+ ): string | readonly string[] =>
61
+ typeof path === "string" ? path : Object.freeze([...path]);
62
+
63
+ const asPath = (path: string | readonly string[]): readonly string[] =>
64
+ typeof path === "string" ? [path] : path;
65
+
66
+ const pathsEqual = (
67
+ left: string | readonly string[],
68
+ right: string | readonly string[],
69
+ ): boolean => {
70
+ const leftPath = asPath(left);
71
+ const rightPath = asPath(right);
72
+ return (
73
+ leftPath.length === rightPath.length &&
74
+ leftPath.every((segment, index) => segment === rightPath[index])
75
+ );
76
+ };
77
+
78
+ const getFieldValue = (value: unknown, path: string | readonly string[]) => {
79
+ let current = value as Record<string, unknown> | undefined;
80
+ for (const segment of asPath(path)) {
81
+ if (current == null) {
82
+ return undefined;
83
+ }
84
+ current = current[segment] as Record<string, unknown> | undefined;
85
+ }
86
+ return current;
87
+ };
88
+
89
+ const setFieldValue = (
90
+ value: Record<string, unknown>,
91
+ path: string | readonly string[],
92
+ fieldValue: unknown,
93
+ ): void => {
94
+ const segments = asPath(path);
95
+ let current = value;
96
+ for (let i = 0; i < segments.length - 1; i++) {
97
+ const segment = segments[i]!;
98
+ const next = current[segment];
99
+ if (!next || typeof next !== "object") {
100
+ const created: Record<string, unknown> = {};
101
+ current[segment] = created;
102
+ current = created;
103
+ } else {
104
+ current = next as Record<string, unknown>;
105
+ }
106
+ }
107
+ current[segments[segments.length - 1]!] = fieldValue;
108
+ };
109
+
110
+ const sourceDescriptor = (
111
+ source: DocumentTransformSource,
112
+ ): DocumentTransformSourceDescriptor =>
113
+ typeof source === "object" && !Array.isArray(source) && "kind" in source
114
+ ? source
115
+ : { kind: "field", path: freezePath(source) };
116
+
117
+ const readSource = (
118
+ document: unknown,
119
+ context: Context,
120
+ facts: DocumentTransformFacts | undefined,
121
+ source: DocumentTransformSourceDescriptor,
122
+ ): unknown => {
123
+ switch (source.kind) {
124
+ case "field":
125
+ return getFieldValue(document, source.path);
126
+ case "context":
127
+ return context[source.field];
128
+ case "entryFirstSignerPublicKey": {
129
+ const signer = facts?.entryPublicKeys?.[0];
130
+ return signer ? copyBytes(signer.bytes) : undefined;
131
+ }
132
+ }
133
+ };
134
+
135
+ const attachDocumentTransformDescriptor = <T, I>(
136
+ fn: DocumentTransformer<T, I>,
137
+ descriptor: DocumentTransformDescriptor,
138
+ ): DescribedDocumentTransformer<T, I> => {
139
+ Object.defineProperty(fn, NATIVE_DOCUMENT_TRANSFORM, {
140
+ value: Object.freeze(descriptor),
141
+ enumerable: false,
142
+ writable: false,
143
+ configurable: false,
144
+ });
145
+ return fn;
146
+ };
147
+
148
+ export const getDocumentTransformDescriptor = <T, I>(
149
+ transformer: DocumentTransformer<T, I> | undefined,
150
+ ): DocumentTransformDescriptor | undefined =>
151
+ (transformer as DescribedDocumentTransformer<T, I> | undefined)?.[
152
+ NATIVE_DOCUMENT_TRANSFORM
153
+ ];
154
+
155
+ export const canPrepareDocumentTransformBeforeAppend = (
156
+ descriptor: DocumentTransformDescriptor | undefined,
157
+ ): boolean => {
158
+ if (!descriptor) {
159
+ return false;
160
+ }
161
+ switch (descriptor.kind) {
162
+ case "identity":
163
+ case "pick":
164
+ return true;
165
+ case "project":
166
+ return descriptor.fields.every(
167
+ (field) => field.source.kind !== "context",
168
+ );
169
+ }
170
+ };
171
+
172
+ export const canPrepareDocumentTransformWithAppendFacts = (
173
+ descriptor: DocumentTransformDescriptor | undefined,
174
+ ): boolean => {
175
+ if (!descriptor) {
176
+ return false;
177
+ }
178
+ switch (descriptor.kind) {
179
+ case "identity":
180
+ case "pick":
181
+ return true;
182
+ case "project":
183
+ return descriptor.fields.every(
184
+ (field) =>
185
+ field.source.kind !== "context" || field.source.field !== "head",
186
+ );
187
+ }
188
+ };
189
+
190
+ export const documentTransformPreservesFieldPath = (
191
+ descriptor: DocumentTransformDescriptor | undefined,
192
+ path: string | readonly string[],
193
+ ): boolean => {
194
+ if (!descriptor) {
195
+ return false;
196
+ }
197
+ switch (descriptor.kind) {
198
+ case "identity":
199
+ return true;
200
+ case "pick":
201
+ return descriptor.fields.some((field) => pathsEqual(field, path));
202
+ case "project":
203
+ return descriptor.fields.some(
204
+ (field) =>
205
+ pathsEqual(field.target, path) &&
206
+ field.source.kind === "field" &&
207
+ pathsEqual(field.source.path, path),
208
+ );
209
+ }
210
+ };
211
+
212
+ export const transform = {
213
+ identity: <T = unknown>(): DocumentTransformer<T, T> =>
214
+ attachDocumentTransformDescriptor((obj) => obj, { kind: "identity" }),
215
+
216
+ pick: <
217
+ T = Record<string, unknown>,
218
+ I extends object = Partial<T>,
219
+ >(
220
+ fields: readonly (string | readonly string[])[],
221
+ ): DocumentTransformer<T, I> => {
222
+ const frozenFields = Object.freeze(fields.map(freezePath));
223
+ return attachDocumentTransformDescriptor(
224
+ (document) => {
225
+ const out: Record<string, unknown> = {};
226
+ for (const path of frozenFields) {
227
+ setFieldValue(out, path, getFieldValue(document, path));
228
+ }
229
+ return out as I;
230
+ },
231
+ { kind: "pick", fields: frozenFields },
232
+ );
233
+ },
234
+
235
+ field: (
236
+ path: string | readonly string[],
237
+ ): DocumentTransformSourceDescriptor => ({
238
+ kind: "field",
239
+ path: freezePath(path),
240
+ }),
241
+
242
+ context: (
243
+ field: "created" | "modified" | "head" | "gid" | "size",
244
+ ): DocumentTransformSourceDescriptor => ({ kind: "context", field }),
245
+
246
+ entryFirstSignerPublicKey:
247
+ (): DocumentTransformSourceDescriptor => ({
248
+ kind: "entryFirstSignerPublicKey",
249
+ }),
250
+
251
+ project: <T = unknown, I extends object = Record<string, unknown>>(
252
+ fields: Record<string, DocumentTransformSource>,
253
+ ): DocumentTransformer<T, I> => {
254
+ const entries = Object.freeze(
255
+ Object.entries(fields).map(([target, source]) =>
256
+ Object.freeze({
257
+ target,
258
+ source: Object.freeze(sourceDescriptor(source)),
259
+ }),
260
+ ),
261
+ );
262
+ return attachDocumentTransformDescriptor(
263
+ (document, context, facts) => {
264
+ const out: Record<string, unknown> = {};
265
+ for (const entry of entries) {
266
+ setFieldValue(
267
+ out,
268
+ entry.target,
269
+ readSource(document, context, facts, entry.source),
270
+ );
271
+ }
272
+ return out as I;
273
+ },
274
+ { kind: "project", fields: entries },
275
+ );
276
+ },
277
+ } as const;