@visiolab/effect-firebase 0.0.0-stage → 0.3.1
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 +81 -2
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/app/App.d.mts +27 -0
- package/dist/app/App.mjs +27 -0
- package/dist/app/index.d.mts +2 -0
- package/dist/app/index.mjs +1 -0
- package/dist/firestore/Collection.d.mts +423 -0
- package/dist/firestore/Collection.mjs +133 -0
- package/dist/firestore/CollectionGroup.d.mts +164 -0
- package/dist/firestore/CollectionGroup.mjs +84 -0
- package/dist/firestore/DocumentPath.d.mts +95 -0
- package/dist/firestore/DocumentPath.mjs +71 -0
- package/dist/firestore/DocumentSnapshot.d.mts +239 -0
- package/dist/firestore/DocumentSnapshot.mjs +134 -0
- package/dist/firestore/FieldValue.d.mts +332 -0
- package/dist/firestore/FieldValue.mjs +256 -0
- package/dist/firestore/Firestore.d.mts +1165 -0
- package/dist/firestore/Firestore.mjs +274 -0
- package/dist/firestore/FirestoreModel.d.mts +1232 -0
- package/dist/firestore/FirestoreModel.mjs +586 -0
- package/dist/firestore/FirestoreSchema.d.mts +567 -0
- package/dist/firestore/FirestoreSchema.mjs +419 -0
- package/dist/firestore/FirestoreValue.d.mts +145 -0
- package/dist/firestore/FirestoreValue.mjs +87 -0
- package/dist/firestore/GeoPoint.d.mts +90 -0
- package/dist/firestore/GeoPoint.mjs +75 -0
- package/dist/firestore/Patch.d.mts +73 -0
- package/dist/firestore/Patch.mjs +57 -0
- package/dist/firestore/QuerySnapshot.d.mts +107 -0
- package/dist/firestore/QuerySnapshot.mjs +43 -0
- package/dist/firestore/Timestamp.d.mts +273 -0
- package/dist/firestore/Timestamp.mjs +255 -0
- package/dist/firestore/index.d.mts +14 -0
- package/dist/firestore/index.mjs +14 -0
- package/dist/firestore/internal/collection.mjs +156 -0
- package/dist/firestore/internal/collectionGroup.mjs +64 -0
- package/dist/firestore/internal/documentPath.d.mts +4 -0
- package/dist/firestore/internal/documentPath.mjs +42 -0
- package/dist/firestore/internal/documentSnapshot.mjs +63 -0
- package/dist/firestore/internal/fieldValue.d.mts +11 -0
- package/dist/firestore/internal/fieldValue.mjs +88 -0
- package/dist/firestore/internal/firestore.mjs +145 -0
- package/dist/firestore/internal/firestoreError.d.mts +29 -0
- package/dist/firestore/internal/firestoreError.mjs +30 -0
- package/dist/firestore/internal/firestoreModel.mjs +719 -0
- package/dist/firestore/internal/firestoreSchema.mjs +123 -0
- package/dist/firestore/internal/firestoreValue.mjs +108 -0
- package/dist/firestore/internal/firestoreValueWalk.d.mts +58 -0
- package/dist/firestore/internal/firestoreValueWalk.mjs +223 -0
- package/dist/firestore/internal/geoPoint.d.mts +4 -0
- package/dist/firestore/internal/geoPoint.mjs +39 -0
- package/dist/firestore/internal/patch.mjs +19 -0
- package/dist/firestore/internal/querySnapshot.mjs +24 -0
- package/dist/firestore/internal/schemaAST.mjs +35 -0
- package/dist/firestore/internal/snapshotExtra.mjs +35 -0
- package/dist/firestore/internal/streamFromListener.mjs +12 -0
- package/dist/firestore/internal/timestamp.d.mts +4 -0
- package/dist/firestore/internal/timestamp.mjs +66 -0
- package/dist/firestore/internal/timestampSchema.mjs +31 -0
- package/dist/firestore/internal/transactionFromCallback.mjs +66 -0
- package/dist/functions/Callable.d.mts +654 -0
- package/dist/functions/Callable.mjs +450 -0
- package/dist/functions/CallableError.d.mts +297 -0
- package/dist/functions/CallableError.mjs +256 -0
- package/dist/functions/CallableMiddleware.d.mts +213 -0
- package/dist/functions/CallableMiddleware.mjs +52 -0
- package/dist/functions/CallableRequest.d.mts +166 -0
- package/dist/functions/CallableRequest.mjs +18 -0
- package/dist/functions/Caller.d.mts +103 -0
- package/dist/functions/Caller.mjs +115 -0
- package/dist/functions/DocumentTrigger.d.mts +795 -0
- package/dist/functions/DocumentTrigger.mjs +566 -0
- package/dist/functions/FirebaseFunction.d.mts +79 -0
- package/dist/functions/FirebaseFunction.mjs +4 -0
- package/dist/functions/FunctionGroup.d.mts +175 -0
- package/dist/functions/FunctionGroup.mjs +64 -0
- package/dist/functions/index.d.mts +9 -0
- package/dist/functions/index.mjs +9 -0
- package/dist/functions/internal/callable.mjs +167 -0
- package/dist/functions/internal/callableMiddleware.d.mts +19 -0
- package/dist/functions/internal/callableMiddleware.mjs +15 -0
- package/dist/functions/internal/callableRequest.mjs +6 -0
- package/dist/functions/internal/documentTrigger.d.mts +10 -0
- package/dist/functions/internal/documentTrigger.mjs +73 -0
- package/dist/functions/internal/firebaseFunction.d.mts +5 -0
- package/dist/functions/internal/firebaseFunction.mjs +24 -0
- package/dist/functions/internal/functionGroup.mjs +36 -0
- package/package.json +72 -3
- package/src/app/App.ts +24 -0
- package/src/app/index.ts +7 -0
- package/src/firestore/Collection.ts +520 -0
- package/src/firestore/CollectionGroup.ts +245 -0
- package/src/firestore/DocumentPath.ts +114 -0
- package/src/firestore/DocumentSnapshot.ts +308 -0
- package/src/firestore/FieldValue.ts +375 -0
- package/src/firestore/Firestore.ts +1432 -0
- package/src/firestore/FirestoreModel.ts +1884 -0
- package/src/firestore/FirestoreSchema.ts +643 -0
- package/src/firestore/FirestoreValue.ts +207 -0
- package/src/firestore/GeoPoint.ts +105 -0
- package/src/firestore/Patch.ts +117 -0
- package/src/firestore/QuerySnapshot.ts +150 -0
- package/src/firestore/Timestamp.ts +302 -0
- package/src/firestore/index.ts +67 -0
- package/src/firestore/internal/collection.ts +246 -0
- package/src/firestore/internal/collectionGroup.ts +81 -0
- package/src/firestore/internal/documentPath.ts +53 -0
- package/src/firestore/internal/documentSnapshot.ts +117 -0
- package/src/firestore/internal/fieldValue.ts +119 -0
- package/src/firestore/internal/firestore.ts +322 -0
- package/src/firestore/internal/firestoreError.ts +36 -0
- package/src/firestore/internal/firestoreModel.ts +1242 -0
- package/src/firestore/internal/firestoreSchema.ts +218 -0
- package/src/firestore/internal/firestoreValue.ts +202 -0
- package/src/firestore/internal/firestoreValueWalk.ts +349 -0
- package/src/firestore/internal/geoPoint.ts +59 -0
- package/src/firestore/internal/patch.ts +21 -0
- package/src/firestore/internal/querySnapshot.ts +39 -0
- package/src/firestore/internal/schemaAST.ts +83 -0
- package/src/firestore/internal/snapshotExtra.ts +47 -0
- package/src/firestore/internal/streamFromListener.ts +27 -0
- package/src/firestore/internal/timestamp.ts +119 -0
- package/src/firestore/internal/timestampSchema.ts +42 -0
- package/src/firestore/internal/transactionFromCallback.ts +106 -0
- package/src/functions/Callable.ts +855 -0
- package/src/functions/CallableError.ts +348 -0
- package/src/functions/CallableMiddleware.ts +279 -0
- package/src/functions/CallableRequest.ts +207 -0
- package/src/functions/Caller.ts +141 -0
- package/src/functions/DocumentTrigger.ts +927 -0
- package/src/functions/FirebaseFunction.ts +96 -0
- package/src/functions/FunctionGroup.ts +225 -0
- package/src/functions/index.ts +62 -0
- package/src/functions/internal/callable.ts +272 -0
- package/src/functions/internal/callableMiddleware.ts +54 -0
- package/src/functions/internal/callableRequest.ts +8 -0
- package/src/functions/internal/documentTrigger.ts +143 -0
- package/src/functions/internal/firebaseFunction.ts +58 -0
- package/src/functions/internal/functionGroup.ts +47 -0
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Effect,
|
|
3
|
+
Predicate,
|
|
4
|
+
Schema,
|
|
5
|
+
SchemaAST,
|
|
6
|
+
SchemaGetter,
|
|
7
|
+
SchemaIssue,
|
|
8
|
+
Result,
|
|
9
|
+
SchemaParser,
|
|
10
|
+
} from "effect";
|
|
11
|
+
import { VariantSchema } from "effect/schema";
|
|
12
|
+
import type * as FirestoreModel from "../FirestoreModel.ts";
|
|
13
|
+
import type * as FirestoreSchema from "../FirestoreSchema.ts";
|
|
14
|
+
import * as fieldValue from "./fieldValue.ts";
|
|
15
|
+
import * as firestoreSchema from "./firestoreSchema.ts";
|
|
16
|
+
import * as firestoreValue from "./firestoreValue.ts";
|
|
17
|
+
import * as firestoreValueWalk from "./firestoreValueWalk.ts";
|
|
18
|
+
import * as patch from "./patch.ts";
|
|
19
|
+
|
|
20
|
+
const Variants = VariantSchema.make({
|
|
21
|
+
variants: ["read", "create", "update"],
|
|
22
|
+
defaultVariant: "read",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** @internal */
|
|
26
|
+
export const Field = Variants.Field;
|
|
27
|
+
|
|
28
|
+
/** @internal */
|
|
29
|
+
export const FieldOnly = Variants.FieldOnly;
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
export const FieldExcept = Variants.FieldExcept;
|
|
33
|
+
|
|
34
|
+
/** @internal */
|
|
35
|
+
export const Struct = Variants.Struct;
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export const fieldEvolve = Variants.fieldEvolve;
|
|
39
|
+
|
|
40
|
+
/** @internal */
|
|
41
|
+
export const extract = Variants.extract;
|
|
42
|
+
|
|
43
|
+
/** @internal */
|
|
44
|
+
export const UnionTypeId = "~@visiolab/effect-firebase/firestore/FirestoreModel/Union";
|
|
45
|
+
|
|
46
|
+
// What `VariantSchema` tells a field by, which a union carries to be extracted
|
|
47
|
+
// variant by variant inside a struct, as a field is. Effect does not export
|
|
48
|
+
// it, so it is written out here: one more reliance on Effect internals of the
|
|
49
|
+
// kind ADR 0002 accepts, held safe by `effect` being pinned to an exact version.
|
|
50
|
+
const FieldTypeId = "~effect/schema/VariantSchema/Field";
|
|
51
|
+
|
|
52
|
+
type Members = ReadonlyArray<VariantSchema.Struct<any>>;
|
|
53
|
+
|
|
54
|
+
/** @internal */
|
|
55
|
+
export const fields = <A extends VariantSchema.Struct<any>>(
|
|
56
|
+
self: A,
|
|
57
|
+
): A[typeof VariantSchema.TypeId] => VariantSchema.fields(self);
|
|
58
|
+
|
|
59
|
+
/** @internal */
|
|
60
|
+
export const RecordTypeId = "~@visiolab/effect-firebase/firestore/FirestoreModel/Record";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where an update goes one level down when flattening: at the keys of a struct,
|
|
64
|
+
* or at every key of a Record, whatever it is. A key missing from both is a
|
|
65
|
+
* value, written whole.
|
|
66
|
+
*/
|
|
67
|
+
interface Shape {
|
|
68
|
+
readonly fields: { readonly [key: string]: Shape };
|
|
69
|
+
readonly rest: { readonly value: Shape | undefined } | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The keys of a Record that only the data names, and what each of them holds. */
|
|
73
|
+
interface Rest<A> {
|
|
74
|
+
readonly key: Schema.Top;
|
|
75
|
+
readonly value: A;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A path an update writes at, one segment per key: the name of a field, or the
|
|
80
|
+
* schema of the key of a Record, which encodes whatever key sits there.
|
|
81
|
+
*/
|
|
82
|
+
type Path = ReadonlyArray<string | Schema.Top>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The schemas an update writes at: by path where it only goes through fields,
|
|
86
|
+
* and by the pattern of the paths where it goes through a Record.
|
|
87
|
+
*/
|
|
88
|
+
interface Paths {
|
|
89
|
+
readonly known: Record<string, Schema.Top>;
|
|
90
|
+
readonly patterns: Map<string, Schema.Top>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** No path yet, to gather an update's into. */
|
|
94
|
+
const emptyPaths = (): Paths => ({ known: {}, patterns: new Map() });
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Throws on a field name holding a dot, which a path would read as two.
|
|
98
|
+
*/
|
|
99
|
+
const assertFieldName = (key: string): void => {
|
|
100
|
+
if (key.includes(".")) {
|
|
101
|
+
throw new Error(`Invalid Firestore field name "${key}": it must not contain "."`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** The reason a key is not one a path can name on its own, if it is not. */
|
|
106
|
+
const unaddressable = (key: string): string | undefined =>
|
|
107
|
+
key === "" ? "must not be empty" : key.includes(".") ? 'must not contain "."' : undefined;
|
|
108
|
+
|
|
109
|
+
/** A key a path names on its own, which is a non-empty one holding no dot. */
|
|
110
|
+
const Segment = Schema.String.check(Schema.isPattern(/^[^.]+$/));
|
|
111
|
+
|
|
112
|
+
/** Where a path sits, with `*` for each key only the data names. */
|
|
113
|
+
const where = (path: Path): string =>
|
|
114
|
+
path.map((segment) => (Schema.isSchema(segment) ? "*" : segment)).join(".");
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The parts of the template literal a path matches: the names of the fields
|
|
118
|
+
* along it, joined by dots, and `key` for each key of a Record.
|
|
119
|
+
*/
|
|
120
|
+
const template = <A>(path: Path, key: (schema: Schema.Top, i: number) => A): Array<string | A> => {
|
|
121
|
+
const parts: Array<string | A> = [];
|
|
122
|
+
let literal = "";
|
|
123
|
+
path.forEach((segment, i) => {
|
|
124
|
+
if (i > 0) {
|
|
125
|
+
literal += ".";
|
|
126
|
+
}
|
|
127
|
+
if (Schema.isSchema(segment)) {
|
|
128
|
+
if (literal !== "") {
|
|
129
|
+
parts.push(literal);
|
|
130
|
+
}
|
|
131
|
+
parts.push(key(segment, i));
|
|
132
|
+
literal = "";
|
|
133
|
+
} else {
|
|
134
|
+
literal += segment;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (literal !== "") {
|
|
138
|
+
parts.push(literal);
|
|
139
|
+
}
|
|
140
|
+
return parts;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The key of a pattern of paths, as an update is written with it and as
|
|
145
|
+
* Firestore holds it: each key of a Record is encoded by the Record's key
|
|
146
|
+
* schema, and refused where its encoded form is one no path can name.
|
|
147
|
+
*/
|
|
148
|
+
const PathKey = (path: Path): Schema.Top => {
|
|
149
|
+
const encoded = template(path, (key, i) =>
|
|
150
|
+
Schema.String.check(
|
|
151
|
+
Schema.makeFilter((segment: string) => {
|
|
152
|
+
const reason = unaddressable(segment);
|
|
153
|
+
return reason === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: `Invalid Firestore field name "${segment}" in "${where(path.slice(0, i))}": it ${reason}`;
|
|
156
|
+
}),
|
|
157
|
+
).pipe(Schema.decodeTo(key as Schema.Codec<unknown, string>)),
|
|
158
|
+
);
|
|
159
|
+
const decoded = template(path, (key) => (recordKeys(key).rest ? Segment : Schema.toType(key)));
|
|
160
|
+
const parser = Schema.TemplateLiteralParser(encoded as any) as unknown as Schema.Codec<
|
|
161
|
+
ReadonlyArray<unknown>,
|
|
162
|
+
string
|
|
163
|
+
>;
|
|
164
|
+
return parser.pipe(
|
|
165
|
+
Schema.decodeTo(Schema.TemplateLiteral(decoded as any) as Schema.Codec<string, string>, {
|
|
166
|
+
decode: SchemaGetter.transform((parts) => parts.join("")),
|
|
167
|
+
// A key a path names holds no dot, so splitting the path at each one
|
|
168
|
+
// gives back its keys.
|
|
169
|
+
encode: SchemaGetter.transform((key) => {
|
|
170
|
+
const segments = key.split(".");
|
|
171
|
+
return template(path, (_, i) => segments[i]);
|
|
172
|
+
}),
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Every key of a Record known up front sits at the same pattern, which is only
|
|
178
|
+
// added once. A pattern holds no optional key, since any path it matches may be
|
|
179
|
+
// left out anyway.
|
|
180
|
+
const addPath = (paths: Paths, path: Path, schema: Schema.Top): void => {
|
|
181
|
+
if (path.every((segment) => typeof segment === "string")) {
|
|
182
|
+
paths.known[path.join(".")] = Schema.optionalKey(schema);
|
|
183
|
+
} else if (!paths.patterns.has(where(path))) {
|
|
184
|
+
paths.patterns.set(
|
|
185
|
+
where(path),
|
|
186
|
+
Schema.Record(PathKey(path) as Schema.Record.Key, withoutOptionalKey(schema)),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const variantSchema = (
|
|
192
|
+
field: unknown,
|
|
193
|
+
variant: "read" | "create" | "update",
|
|
194
|
+
): Schema.Top | undefined =>
|
|
195
|
+
VariantSchema.isStruct(field)
|
|
196
|
+
? (extract(field, variant) as Schema.Top)
|
|
197
|
+
: VariantSchema.isField(field)
|
|
198
|
+
? (field.schemas as any)[variant]
|
|
199
|
+
: (field as Schema.Top);
|
|
200
|
+
|
|
201
|
+
const updateSchema = (field: unknown): Schema.Top | undefined =>
|
|
202
|
+
VariantSchema.isStruct(field) ? undefined : variantSchema(field, "update");
|
|
203
|
+
|
|
204
|
+
// What Effect itself strips a default with, though it is left out of the
|
|
205
|
+
// typings.
|
|
206
|
+
const replaceContext = (SchemaAST as any).replaceContext as <A extends SchemaAST.AST>(
|
|
207
|
+
ast: A,
|
|
208
|
+
context: SchemaAST.Context,
|
|
209
|
+
) => A;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Drops the default a field is constructed with. A patch leaves a key out to
|
|
213
|
+
* keep what the document holds, so filling one in would overwrite it.
|
|
214
|
+
*/
|
|
215
|
+
const withoutConstructorDefault = (schema: Schema.Top): Schema.Top => {
|
|
216
|
+
const context = schema.ast.context;
|
|
217
|
+
if (context?.constructorDefault === undefined) {
|
|
218
|
+
return schema;
|
|
219
|
+
}
|
|
220
|
+
return Schema.make(
|
|
221
|
+
replaceContext(
|
|
222
|
+
schema.ast,
|
|
223
|
+
new SchemaAST.Context(context.isOptional, context.isMutable, undefined, context.annotations),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Drops the optional key a schema is made, for a value at a key only the data
|
|
230
|
+
* names, which may be missing whatever it holds.
|
|
231
|
+
*/
|
|
232
|
+
const withoutOptionalKey = (schema: Schema.Top): Schema.Top => {
|
|
233
|
+
const context = schema.ast.context;
|
|
234
|
+
if (context?.isOptional !== true) {
|
|
235
|
+
return schema;
|
|
236
|
+
}
|
|
237
|
+
return Schema.make(
|
|
238
|
+
replaceContext(
|
|
239
|
+
schema.ast,
|
|
240
|
+
new SchemaAST.Context(
|
|
241
|
+
false,
|
|
242
|
+
context.isMutable,
|
|
243
|
+
context.constructorDefault,
|
|
244
|
+
context.annotations,
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** The keys a Record key schema names up front, and whether the data names more. */
|
|
251
|
+
const recordKeys = (key: Schema.Top): { readonly known: Array<string>; readonly rest: boolean } => {
|
|
252
|
+
const ast = Schema.Record(key as Schema.Record.Key, Schema.Unknown).ast;
|
|
253
|
+
const known = ast.propertySignatures.map(({ name }) => {
|
|
254
|
+
if (typeof name === "symbol") {
|
|
255
|
+
throw new Error(`Invalid Firestore field name ${String(name)}: it must be a string`);
|
|
256
|
+
}
|
|
257
|
+
const segment = String(name);
|
|
258
|
+
const reason = unaddressable(segment);
|
|
259
|
+
if (reason !== undefined) {
|
|
260
|
+
throw new Error(`Invalid Firestore field name "${segment}": it ${reason}`);
|
|
261
|
+
}
|
|
262
|
+
return segment;
|
|
263
|
+
});
|
|
264
|
+
const rest = ast.indexSignatures.length > 0;
|
|
265
|
+
if (rest && known.length > 0) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Invalid Record key: it names ${known.map((k) => `"${k}"`).join(", ")} besides keys only the data names, which an update cannot tell apart`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return { known, rest };
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* What an update reaches into: a struct, or a Record. Either holds fields at
|
|
275
|
+
* keys known up front, and a Record may hold more at keys only the data names.
|
|
276
|
+
*/
|
|
277
|
+
interface Node {
|
|
278
|
+
readonly fields: VariantSchema.Struct.Fields;
|
|
279
|
+
/**
|
|
280
|
+
* The members of a union, which has no fields of its own to reach into: it
|
|
281
|
+
* is written whole.
|
|
282
|
+
*/
|
|
283
|
+
readonly members?: Members | undefined;
|
|
284
|
+
/**
|
|
285
|
+
* The key schema of a Record keyed by literals, which a path names each of
|
|
286
|
+
* its fields by, to encode it as part of the update.
|
|
287
|
+
*/
|
|
288
|
+
readonly key: Schema.Top | undefined;
|
|
289
|
+
readonly rest: Rest<unknown> | undefined;
|
|
290
|
+
/** The whole node, as `set` writes it. */
|
|
291
|
+
readonly create: () => Schema.Top;
|
|
292
|
+
/** Whether a document may go without it. */
|
|
293
|
+
readonly optional: boolean;
|
|
294
|
+
/** Whether an update writes anything at all into it. */
|
|
295
|
+
readonly writable: boolean;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const recordNode = (
|
|
299
|
+
key: Schema.Top,
|
|
300
|
+
value: unknown,
|
|
301
|
+
create: () => Schema.Top,
|
|
302
|
+
optional: boolean,
|
|
303
|
+
): Node => {
|
|
304
|
+
const keys = recordKeys(key);
|
|
305
|
+
return {
|
|
306
|
+
fields: Object.fromEntries(keys.known.map((k) => [k, value])) as VariantSchema.Struct.Fields,
|
|
307
|
+
key: keys.rest ? undefined : key,
|
|
308
|
+
rest: keys.rest ? { key, value } : undefined,
|
|
309
|
+
create,
|
|
310
|
+
optional,
|
|
311
|
+
// A value no update writes leaves nothing of the Record to write either.
|
|
312
|
+
writable: nodeOf(value)?.writable ?? updateSchema(value) !== undefined,
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/** Whether a field was made {@link FirestoreModel.Optional}. */
|
|
317
|
+
const isOptional = (field: unknown): field is { readonly field: unknown } =>
|
|
318
|
+
Predicate.hasProperty(field, "~optionality") && field["~optionality"] === "optional";
|
|
319
|
+
|
|
320
|
+
/** What a field reaches into, if it is a struct or a Record. */
|
|
321
|
+
const nodeOf = (field: unknown, optional = false): Node | undefined => {
|
|
322
|
+
if (isOptional(field)) {
|
|
323
|
+
return nodeOf(field.field, true);
|
|
324
|
+
}
|
|
325
|
+
if (Predicate.hasProperty(field, UnionTypeId)) {
|
|
326
|
+
return unionNode(field[UnionTypeId] as Members, optional);
|
|
327
|
+
}
|
|
328
|
+
if (Predicate.hasProperty(field, RecordTypeId)) {
|
|
329
|
+
const { key, value } = field[RecordTypeId] as Rest<unknown>;
|
|
330
|
+
return recordNode(key, value, () => variantSchema(field, "create")!, optional);
|
|
331
|
+
}
|
|
332
|
+
if (VariantSchema.isStruct(field)) {
|
|
333
|
+
return {
|
|
334
|
+
fields: VariantSchema.fields(field),
|
|
335
|
+
key: undefined,
|
|
336
|
+
rest: undefined,
|
|
337
|
+
create: () => extract(field, "create") as Schema.Top,
|
|
338
|
+
optional,
|
|
339
|
+
writable: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return undefined;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whether a set of fields may be written whole by an update: only where every
|
|
347
|
+
* field it holds, at any depth, is written both by `set`, whose shape a
|
|
348
|
+
* replacement takes, and by an update. Replacing one holding a field written
|
|
349
|
+
* once would change it, and one holding a field `set` leaves out would drop it.
|
|
350
|
+
*/
|
|
351
|
+
const isReplaceable = (fields: VariantSchema.Struct.Fields): boolean =>
|
|
352
|
+
Object.values(fields).every((field) => {
|
|
353
|
+
const node = nodeOf(field);
|
|
354
|
+
return node === undefined
|
|
355
|
+
? variantSchema(field, "create") !== undefined && updateSchema(field) !== undefined
|
|
356
|
+
: isNodeReplaceable(node);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const isNodeReplaceable = (node: Node): boolean =>
|
|
360
|
+
node.members !== undefined
|
|
361
|
+
? node.members.every(isMemberReplaceable)
|
|
362
|
+
: isReplaceable(node.fields) &&
|
|
363
|
+
(node.rest === undefined ||
|
|
364
|
+
isReplaceable({ _: node.rest.value } as VariantSchema.Struct.Fields));
|
|
365
|
+
|
|
366
|
+
const isMemberReplaceable = (member: VariantSchema.Struct<any>): boolean =>
|
|
367
|
+
isReplaceable(fields(member));
|
|
368
|
+
|
|
369
|
+
/** The union of what `set` writes each of `members` as. */
|
|
370
|
+
const unionCreate = (members: Members): Schema.Top =>
|
|
371
|
+
Schema.Union(members.map((member) => extract(member, "create") as Schema.Top));
|
|
372
|
+
|
|
373
|
+
const unionNode = (members: Members, optional: boolean): Node => ({
|
|
374
|
+
fields: {},
|
|
375
|
+
members,
|
|
376
|
+
key: undefined,
|
|
377
|
+
rest: undefined,
|
|
378
|
+
create: () => unionCreate(members),
|
|
379
|
+
optional,
|
|
380
|
+
writable: optional || members.some(isMemberReplaceable) || hasSharedField(members.map(fields)),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The replacement of a struct, holding the struct as `create` makes it - with
|
|
385
|
+
* its constructor defaults filled in, since a default left out of a struct
|
|
386
|
+
* written whole would be a field dropped.
|
|
387
|
+
*
|
|
388
|
+
* Given several, as the unions the members of a union hold at one key are, it
|
|
389
|
+
* is made by every one of them, each of which must take it, and holds it as
|
|
390
|
+
* the first makes it. That they also store it the same way is for the schema
|
|
391
|
+
* of its path to tell - see {@link SharedValueSchema}.
|
|
392
|
+
*/
|
|
393
|
+
const ReplaceSchema = (creates: ReadonlyArray<Schema.Top>): Schema.Top =>
|
|
394
|
+
Schema.declareConstructor<any, any>()(
|
|
395
|
+
creates,
|
|
396
|
+
(creates) => (input, ast, options) => {
|
|
397
|
+
if (!patch.isReplace(input)) {
|
|
398
|
+
return Effect.fail(new SchemaIssue.InvalidType(ast, input, options));
|
|
399
|
+
}
|
|
400
|
+
return Effect.forEach(creates, (create) => create.makeEffect(input.value, options)).pipe(
|
|
401
|
+
Effect.mapBothEager({
|
|
402
|
+
onSuccess: ([made]) => patch.replace(made),
|
|
403
|
+
onFailure: (issue) => new SchemaIssue.Pointer(["value"], issue),
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
},
|
|
407
|
+
{ expected: "a struct written whole" },
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The patch of a struct, refusing the markers a `Schema.Struct` would
|
|
412
|
+
* otherwise take for a patch of nothing, since it keeps only the keys it knows.
|
|
413
|
+
*/
|
|
414
|
+
const StructPatchSchema = (fields: Schema.Top): Schema.Top =>
|
|
415
|
+
Schema.declareConstructor<any, any>()(
|
|
416
|
+
[fields],
|
|
417
|
+
([fields]) =>
|
|
418
|
+
(input, ast, options) =>
|
|
419
|
+
patch.isReplace(input) || fieldValue.isDeleteField(input)
|
|
420
|
+
? Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
|
|
421
|
+
: SchemaParser.decodeUnknownEffect(fields)(input, options),
|
|
422
|
+
{ expected: "a patch of a struct" },
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Builds the patch a set of fields is updated through in `mode`, along with
|
|
427
|
+
* where its structs and Records sit, and gathers the schema of each path it can
|
|
428
|
+
* write at into `paths`.
|
|
429
|
+
*
|
|
430
|
+
* A struct takes a patch, written at its paths, unless it may be missing and
|
|
431
|
+
* the patch is checked, since the patch would create it holding only what it
|
|
432
|
+
* carried. It takes a replacement wherever it is replaceable, and a removal
|
|
433
|
+
* where it may be missing. A Record is a struct whose keys may come from the
|
|
434
|
+
* data - see {@link buildNode}.
|
|
435
|
+
*
|
|
436
|
+
* Where `recordKey` is given, the fields are the keys of a Record keyed by
|
|
437
|
+
* literals, and each sits on its path as that key schema encodes it.
|
|
438
|
+
*
|
|
439
|
+
* Throws on a field name holding a dot, which a path would read as two.
|
|
440
|
+
*/
|
|
441
|
+
const build = (
|
|
442
|
+
fields: VariantSchema.Struct.Fields,
|
|
443
|
+
path: Path,
|
|
444
|
+
paths: Paths,
|
|
445
|
+
mode: FirestoreModel.UpdateMode,
|
|
446
|
+
recordKey?: Schema.Top,
|
|
447
|
+
): { readonly patch: Schema.Top; readonly shape: Shape } => {
|
|
448
|
+
const schemas: Record<string, Schema.Top> = {};
|
|
449
|
+
const shapes: Record<string, Shape> = {};
|
|
450
|
+
for (const key of Object.keys(fields)) {
|
|
451
|
+
assertFieldName(key);
|
|
452
|
+
const field = fields[key];
|
|
453
|
+
const at = [...path, recordKey ?? key];
|
|
454
|
+
const node = nodeOf(field);
|
|
455
|
+
if (node !== undefined) {
|
|
456
|
+
if (node.writable) {
|
|
457
|
+
const built = buildNode(node, at, paths, mode);
|
|
458
|
+
schemas[key] = Schema.optionalKey(built.schema);
|
|
459
|
+
shapes[key] = built.shape;
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
const update = updateSchema(field);
|
|
463
|
+
if (update !== undefined) {
|
|
464
|
+
const schema = withoutConstructorDefault(update);
|
|
465
|
+
schemas[key] = Schema.optionalKey(schema);
|
|
466
|
+
addPath(paths, at, schema);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return { patch: Schema.Struct(schemas), shape: { fields: shapes, rest: undefined } };
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Builds what a struct or a Record at `path` takes in `mode`, gathering the
|
|
475
|
+
* paths it writes at into `paths`, the way {@link build} does for a set of
|
|
476
|
+
* fields.
|
|
477
|
+
*
|
|
478
|
+
* A Record whose keys come from the data is patched key by key, each key
|
|
479
|
+
* written at its own path. Any such key may be missing, so each takes the
|
|
480
|
+
* removal, and a value that is itself a struct or a Record is treated as one a
|
|
481
|
+
* document may go without. That also makes a patch safe where the Record
|
|
482
|
+
* itself may be missing: every key it writes is one the Record may hold or not.
|
|
483
|
+
*/
|
|
484
|
+
const buildNode = (
|
|
485
|
+
node: Node,
|
|
486
|
+
path: Path,
|
|
487
|
+
paths: Paths,
|
|
488
|
+
mode: FirestoreModel.UpdateMode,
|
|
489
|
+
): { readonly schema: Schema.Top; readonly shape: Shape } => {
|
|
490
|
+
if (node.members !== undefined) {
|
|
491
|
+
return buildUnion(node, node.members, path, paths, mode);
|
|
492
|
+
}
|
|
493
|
+
const inner =
|
|
494
|
+
node.rest === undefined
|
|
495
|
+
? build(node.fields, path, paths, mode, node.key)
|
|
496
|
+
: buildRest(node.rest, path, paths, mode);
|
|
497
|
+
const create = isNodeReplaceable(node) ? node.create() : undefined;
|
|
498
|
+
const takes: Array<Schema.Top> = [];
|
|
499
|
+
if (!node.optional || mode === "unchecked" || node.rest !== undefined) {
|
|
500
|
+
takes.push(StructPatchSchema(inner.patch));
|
|
501
|
+
}
|
|
502
|
+
if (create !== undefined) {
|
|
503
|
+
takes.push(ReplaceSchema([create]));
|
|
504
|
+
}
|
|
505
|
+
if (node.optional) {
|
|
506
|
+
takes.push(firestoreSchema.DeleteField);
|
|
507
|
+
}
|
|
508
|
+
const written = node.optional
|
|
509
|
+
? create === undefined
|
|
510
|
+
? firestoreSchema.DeleteField
|
|
511
|
+
: firestoreSchema.DeleteFieldOr(create)
|
|
512
|
+
: create;
|
|
513
|
+
if (written !== undefined) {
|
|
514
|
+
addPath(paths, path, written);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
schema: takes.length === 1 ? takes[0] : Schema.Union(takes),
|
|
518
|
+
shape: inner.shape,
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Builds what a union at `path` takes in `mode`, gathering the paths it writes
|
|
524
|
+
* at into `paths`. A union is replaced with any member it may be replaced
|
|
525
|
+
* with, removed where it may be missing, and patched at its Shared Fields -
|
|
526
|
+
* and, unchecked, at a key only some members hold - see {@link buildShared}.
|
|
527
|
+
* The patch is refused where it may be missing and the patch is checked, as it
|
|
528
|
+
* is for a struct. One that takes none of these in `mode` takes nothing.
|
|
529
|
+
*/
|
|
530
|
+
const buildUnion = (
|
|
531
|
+
node: Node,
|
|
532
|
+
members: Members,
|
|
533
|
+
path: Path,
|
|
534
|
+
paths: Paths,
|
|
535
|
+
mode: FirestoreModel.UpdateMode,
|
|
536
|
+
): { readonly schema: Schema.Top; readonly shape: Shape } => {
|
|
537
|
+
const replaceable = members.filter(isMemberReplaceable);
|
|
538
|
+
const create = replaceable.length === 0 ? undefined : unionCreate(replaceable);
|
|
539
|
+
const shared = buildShared(members.map(fields), path, paths, mode);
|
|
540
|
+
const takes: Array<Schema.Top> = [];
|
|
541
|
+
if (shared.keys.length > 0 && (!node.optional || mode === "unchecked")) {
|
|
542
|
+
takes.push(shared.patch);
|
|
543
|
+
}
|
|
544
|
+
if (create !== undefined) {
|
|
545
|
+
takes.push(ReplaceSchema([create]));
|
|
546
|
+
}
|
|
547
|
+
if (node.optional) {
|
|
548
|
+
takes.push(firestoreSchema.DeleteField);
|
|
549
|
+
}
|
|
550
|
+
const written = node.optional
|
|
551
|
+
? create === undefined
|
|
552
|
+
? firestoreSchema.DeleteField
|
|
553
|
+
: firestoreSchema.DeleteFieldOr(create)
|
|
554
|
+
: create;
|
|
555
|
+
if (written !== undefined) {
|
|
556
|
+
addPath(paths, path, written);
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
schema: takes.length === 1 ? takes[0] : Schema.Union(takes),
|
|
560
|
+
shape: shared.shape,
|
|
561
|
+
};
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/** What a field was made {@link FirestoreModel.Optional} of, or the field itself. */
|
|
565
|
+
const unwrap = (field: unknown): unknown => (isOptional(field) ? field.field : field);
|
|
566
|
+
|
|
567
|
+
/** The types a type is a union of, or the type itself. */
|
|
568
|
+
const constituents = (ast: SchemaAST.AST): ReadonlyArray<SchemaAST.AST> =>
|
|
569
|
+
SchemaAST.isUnion(ast) ? ast.types.flatMap(constituents) : [ast];
|
|
570
|
+
|
|
571
|
+
/** What a declaration is told apart by, where it says. */
|
|
572
|
+
const declarationId = (ast: SchemaAST.Declaration): unknown =>
|
|
573
|
+
(ast.annotations?.representation as { readonly id?: unknown } | undefined)?.id;
|
|
574
|
+
|
|
575
|
+
/** The type of a literal a keyword is the type of. */
|
|
576
|
+
const keywordOf = (literal: unknown): string =>
|
|
577
|
+
typeof literal === "string"
|
|
578
|
+
? "String"
|
|
579
|
+
: typeof literal === "number"
|
|
580
|
+
? "Number"
|
|
581
|
+
: typeof literal === "boolean"
|
|
582
|
+
? "Boolean"
|
|
583
|
+
: "BigInt";
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Whether every value of `a` is one of `b`, the way TypeScript tells whether
|
|
587
|
+
* one type extends another, as far as the schemas say. Where they do not say -
|
|
588
|
+
* a declaration with nothing to tell it apart by, a suspended schema - it is
|
|
589
|
+
* taken to be, so the encoding decides.
|
|
590
|
+
*/
|
|
591
|
+
const isSubtype = (a: SchemaAST.AST, b: SchemaAST.AST): boolean => {
|
|
592
|
+
if (SchemaAST.isUnion(a)) {
|
|
593
|
+
return a.types.every((type) => isSubtype(type, b));
|
|
594
|
+
}
|
|
595
|
+
if (SchemaAST.isUnion(b)) {
|
|
596
|
+
return b.types.some((type) => isSubtype(a, type));
|
|
597
|
+
}
|
|
598
|
+
if (SchemaAST.isAny(b) || SchemaAST.isUnknown(b) || SchemaAST.isNever(a) || SchemaAST.isAny(a)) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
if (SchemaAST.isSuspend(a) || SchemaAST.isSuspend(b)) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (SchemaAST.isLiteral(a)) {
|
|
605
|
+
return SchemaAST.isLiteral(b)
|
|
606
|
+
? a.literal === b.literal
|
|
607
|
+
: SchemaAST.isEnum(b)
|
|
608
|
+
? b.enums.some(([, value]) => value === a.literal)
|
|
609
|
+
: SchemaAST.isTemplateLiteral(b)
|
|
610
|
+
? typeof a.literal === "string"
|
|
611
|
+
: b._tag === keywordOf(a.literal);
|
|
612
|
+
}
|
|
613
|
+
if (SchemaAST.isEnum(a)) {
|
|
614
|
+
return a.enums.every(([, value]) => isSubtype(new SchemaAST.Literal(value), b));
|
|
615
|
+
}
|
|
616
|
+
if (SchemaAST.isTemplateLiteral(a)) {
|
|
617
|
+
return SchemaAST.isString(b) || SchemaAST.isTemplateLiteral(b);
|
|
618
|
+
}
|
|
619
|
+
if (SchemaAST.isUniqueSymbol(a)) {
|
|
620
|
+
return SchemaAST.isSymbol(b) || (SchemaAST.isUniqueSymbol(b) && a.symbol === b.symbol);
|
|
621
|
+
}
|
|
622
|
+
if (SchemaAST.isUndefined(a) && SchemaAST.isVoid(b)) {
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
if (SchemaAST.isObjectKeyword(b)) {
|
|
626
|
+
return (
|
|
627
|
+
SchemaAST.isObjectKeyword(a) ||
|
|
628
|
+
SchemaAST.isObjects(a) ||
|
|
629
|
+
SchemaAST.isArrays(a) ||
|
|
630
|
+
SchemaAST.isDeclaration(a)
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (SchemaAST.isDeclaration(a)) {
|
|
634
|
+
if (SchemaAST.isDeclaration(b)) {
|
|
635
|
+
const [ida, idb] = [declarationId(a), declarationId(b)];
|
|
636
|
+
return (
|
|
637
|
+
a === b ||
|
|
638
|
+
ida === undefined ||
|
|
639
|
+
idb === undefined ||
|
|
640
|
+
(ida === idb &&
|
|
641
|
+
a.typeParameters.length === b.typeParameters.length &&
|
|
642
|
+
a.typeParameters.every((p, i) => isSubtype(p, b.typeParameters[i])))
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
// An instance may well have every property a struct asks for.
|
|
646
|
+
return SchemaAST.isObjects(b);
|
|
647
|
+
}
|
|
648
|
+
if (SchemaAST.isObjects(a)) {
|
|
649
|
+
if (SchemaAST.isDeclaration(b)) {
|
|
650
|
+
return declarationId(b) === undefined;
|
|
651
|
+
}
|
|
652
|
+
if (!SchemaAST.isObjects(b)) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
const own = new Map(a.propertySignatures.map((p) => [p.name, p.type]));
|
|
656
|
+
return (
|
|
657
|
+
b.propertySignatures.every((p) => {
|
|
658
|
+
const type = own.get(p.name);
|
|
659
|
+
return type === undefined
|
|
660
|
+
? SchemaAST.isOptional(p.type)
|
|
661
|
+
: (SchemaAST.isOptional(p.type) || !SchemaAST.isOptional(type)) &&
|
|
662
|
+
isSubtype(type, p.type);
|
|
663
|
+
}) &&
|
|
664
|
+
b.indexSignatures.every((index) =>
|
|
665
|
+
[...a.propertySignatures, ...a.indexSignatures].every((p) => isSubtype(p.type, index.type)),
|
|
666
|
+
)
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (SchemaAST.isArrays(a)) {
|
|
670
|
+
if (!SchemaAST.isArrays(b)) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
if (b.elements.length === 0 && b.rest.length === 1) {
|
|
674
|
+
return [...a.elements, ...a.rest].every((type) => isSubtype(type, b.rest[0]));
|
|
675
|
+
}
|
|
676
|
+
if (a.rest.length === 0 && b.rest.length === 0) {
|
|
677
|
+
// A tuple is one of another where each element it may hold is one of
|
|
678
|
+
// the other's at the same place, and the other may go without each
|
|
679
|
+
// element beyond its own.
|
|
680
|
+
return (
|
|
681
|
+
a.elements.length <= b.elements.length &&
|
|
682
|
+
b.elements.every((type, i) => {
|
|
683
|
+
const own = a.elements[i];
|
|
684
|
+
return own === undefined
|
|
685
|
+
? SchemaAST.isOptional(type)
|
|
686
|
+
: (SchemaAST.isOptional(type) || !SchemaAST.isOptional(own)) && isSubtype(own, type);
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
// With a rest, only a tuple shaped alike is told apart element by element.
|
|
691
|
+
return (
|
|
692
|
+
a.elements.length === b.elements.length &&
|
|
693
|
+
a.rest.length === b.rest.length &&
|
|
694
|
+
[...a.elements, ...a.rest].every((type, i) => isSubtype(type, [...b.elements, ...b.rest][i]))
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return a._tag === b._tag;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* The values out of `a` and `b` that the other takes too: each type `a` is a
|
|
702
|
+
* union of that `b` takes whole, and the other way round, as `Meet` works it
|
|
703
|
+
* out in the types.
|
|
704
|
+
*/
|
|
705
|
+
const meet = (
|
|
706
|
+
a: ReadonlyArray<SchemaAST.AST>,
|
|
707
|
+
b: ReadonlyArray<SchemaAST.AST>,
|
|
708
|
+
): ReadonlyArray<SchemaAST.AST> => {
|
|
709
|
+
const within = (types: ReadonlyArray<SchemaAST.AST>) => (type: SchemaAST.AST) =>
|
|
710
|
+
types.some((other) => isSubtype(type, other));
|
|
711
|
+
return [...a.filter(within(b)), ...b.filter(within(a))];
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Whether some value is taken by every one of `schemas`, as `MeetAll` tells it
|
|
716
|
+
* in the types, so a key is shared, and has a path, exactly where its type
|
|
717
|
+
* says. The literal that tells members apart takes none.
|
|
718
|
+
*/
|
|
719
|
+
const haveValueInCommon = (schemas: ReadonlyArray<Schema.Top>): boolean =>
|
|
720
|
+
schemas.map((schema) => constituents(schema.ast)).reduceRight((acc, types) => meet(types, acc))
|
|
721
|
+
.length > 0;
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* What a Shared Field is patched through: the value is encoded through the
|
|
725
|
+
* field of every member, each of which must take it, and all of which must
|
|
726
|
+
* store it as the same Firestore Value. Each member decides by the canonical
|
|
727
|
+
* codec, not by the encoding the caller asks for, since that is what Firestore
|
|
728
|
+
* ends up holding. It is written as the first member writes it, which is then
|
|
729
|
+
* what every member would write.
|
|
730
|
+
*/
|
|
731
|
+
const SharedValueSchema = (schemas: ReadonlyArray<Schema.Top>): Schema.Top => {
|
|
732
|
+
if (schemas.length === 1) {
|
|
733
|
+
return schemas[0];
|
|
734
|
+
}
|
|
735
|
+
// Encoding runs synchronously, as a check does, so a member's encoding may
|
|
736
|
+
// ask for no service.
|
|
737
|
+
const codecs = schemas.map((schema) =>
|
|
738
|
+
firestoreValue.toCodecFirestore(schema as Schema.Codec<unknown, unknown>),
|
|
739
|
+
);
|
|
740
|
+
return schemas[0].check(
|
|
741
|
+
Schema.makeFilter(
|
|
742
|
+
(value: unknown, _, options) => {
|
|
743
|
+
let stored: unknown;
|
|
744
|
+
for (const [i, codec] of codecs.entries()) {
|
|
745
|
+
const result = SchemaParser.encodeUnknownResult(codec)(value, options);
|
|
746
|
+
if (Result.isFailure(result)) {
|
|
747
|
+
return result.failure;
|
|
748
|
+
}
|
|
749
|
+
if (i === 0) {
|
|
750
|
+
stored = result.success;
|
|
751
|
+
} else if (!firestoreValueWalk.equals(stored, result.success)) {
|
|
752
|
+
return "Expected a value every member of the union stores the same way";
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return undefined;
|
|
756
|
+
},
|
|
757
|
+
{ expected: "a value every member of the union takes and stores the same way" },
|
|
758
|
+
),
|
|
759
|
+
);
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* The patch of the Shared Fields of a union, refusing a key it does not know,
|
|
764
|
+
* since a `Schema.Struct` would otherwise drop it and write nothing. That is a
|
|
765
|
+
* key some member goes without, or one the members cannot agree on, such as
|
|
766
|
+
* the literal they are told apart by.
|
|
767
|
+
*/
|
|
768
|
+
const SharedPatchSchema = (fields: Schema.Top, keys: ReadonlyArray<string>): Schema.Top =>
|
|
769
|
+
Schema.declareConstructor<any, any>()(
|
|
770
|
+
[fields],
|
|
771
|
+
([fields]) =>
|
|
772
|
+
(input, ast, options) => {
|
|
773
|
+
if (patch.isReplace(input) || fieldValue.isDeleteField(input)) {
|
|
774
|
+
return Effect.fail(new SchemaIssue.InvalidType(ast, input, options));
|
|
775
|
+
}
|
|
776
|
+
if (Predicate.isObject(input)) {
|
|
777
|
+
const unknown = Object.keys(input).find((key) => !keys.includes(key));
|
|
778
|
+
if (unknown !== undefined) {
|
|
779
|
+
return Effect.fail(
|
|
780
|
+
new SchemaIssue.Pointer(
|
|
781
|
+
[unknown],
|
|
782
|
+
new SchemaIssue.UnexpectedKey(ast, (input as Record<string, unknown>)[unknown]),
|
|
783
|
+
),
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return SchemaParser.decodeUnknownEffect(fields)(input, options);
|
|
788
|
+
},
|
|
789
|
+
{ expected: "a patch of the Shared Fields of a union" },
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Builds the patch of the Shared Fields of a union, given the fields of each
|
|
794
|
+
* of its members, along with where the structs among them sit, and gathers the
|
|
795
|
+
* schema of each path it can write at into `paths`.
|
|
796
|
+
*
|
|
797
|
+
* A Shared Field is a key every member holds, at which every member takes an
|
|
798
|
+
* update: a value, patched through {@link SharedValueSchema}, or a struct,
|
|
799
|
+
* patched at its own Shared Fields by the same rule one level down. A struct
|
|
800
|
+
* every member may go without takes the removal alone, and one only some
|
|
801
|
+
* members may go without takes nothing. A union is shared by the rule a union
|
|
802
|
+
* field is written by - see {@link buildSharedUnion}. A Record is not shared.
|
|
803
|
+
* Where no value is taken by every member's type, as
|
|
804
|
+
* {@link haveValueInCommon} tells, the key is not shared either, which is what
|
|
805
|
+
* keeps it out of `paths` exactly where the type keeps it out.
|
|
806
|
+
*
|
|
807
|
+
* Under `unchecked`, a key only some members hold is patched by the same rule
|
|
808
|
+
* over just those members. Its path is gathered in either mode, so the paths
|
|
809
|
+
* are the same in both and only the patch tells them apart.
|
|
810
|
+
*
|
|
811
|
+
* {@link hasSharedField} tells, without building any of it, whether this
|
|
812
|
+
* takes any key under `unchecked`, and must agree with it.
|
|
813
|
+
*/
|
|
814
|
+
const buildShared = (
|
|
815
|
+
memberFields: ReadonlyArray<VariantSchema.Struct.Fields>,
|
|
816
|
+
path: Path,
|
|
817
|
+
paths: Paths,
|
|
818
|
+
mode: FirestoreModel.UpdateMode,
|
|
819
|
+
): { readonly patch: Schema.Top; readonly shape: Shape; readonly keys: ReadonlyArray<string> } => {
|
|
820
|
+
const schemas: Record<string, Schema.Top> = {};
|
|
821
|
+
const shapes: Record<string, Shape> = {};
|
|
822
|
+
for (const key of keysOf(memberFields)) {
|
|
823
|
+
assertFieldName(key);
|
|
824
|
+
// A key only some members hold is checked against just those, and only
|
|
825
|
+
// patched where the caller vouches for the member. It is written at its
|
|
826
|
+
// path in either mode, as a struct a document may go without is.
|
|
827
|
+
const holding = memberFields.filter((f) => Object.hasOwn(f, key));
|
|
828
|
+
const taken = holding.length === memberFields.length || mode === "unchecked";
|
|
829
|
+
const at = [...path, key];
|
|
830
|
+
const atKey = holding.map((f) => f[key]);
|
|
831
|
+
if (atKey.every(isStructField)) {
|
|
832
|
+
if (atKey.every((field) => !isOptional(field))) {
|
|
833
|
+
const inner = buildShared(atKey.map(structFields), at, paths, mode);
|
|
834
|
+
if (taken && inner.keys.length > 0) {
|
|
835
|
+
schemas[key] = Schema.optionalKey(inner.patch);
|
|
836
|
+
shapes[key] = inner.shape;
|
|
837
|
+
}
|
|
838
|
+
} else if (atKey.every(isOptional)) {
|
|
839
|
+
if (taken) {
|
|
840
|
+
schemas[key] = Schema.optionalKey(firestoreSchema.DeleteField);
|
|
841
|
+
}
|
|
842
|
+
addPath(paths, at, firestoreSchema.DeleteField);
|
|
843
|
+
}
|
|
844
|
+
} else if (atKey.every(isUnionField)) {
|
|
845
|
+
const built = buildSharedUnion(atKey, at, paths, mode);
|
|
846
|
+
if (taken && built !== undefined) {
|
|
847
|
+
schemas[key] = Schema.optionalKey(built.schema);
|
|
848
|
+
shapes[key] = built.shape;
|
|
849
|
+
}
|
|
850
|
+
} else if (atKey.every((field) => !isNodeField(field))) {
|
|
851
|
+
const updates = atKey.map(updateSchema);
|
|
852
|
+
if (updates.every((u) => u !== undefined) && haveValueInCommon(updates)) {
|
|
853
|
+
const schema = SharedValueSchema(updates.map(withoutConstructorDefault));
|
|
854
|
+
if (taken) {
|
|
855
|
+
schemas[key] = Schema.optionalKey(schema);
|
|
856
|
+
}
|
|
857
|
+
addPath(paths, at, schema);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const keys = Object.keys(schemas);
|
|
862
|
+
return {
|
|
863
|
+
patch: SharedPatchSchema(Schema.Struct(schemas), keys),
|
|
864
|
+
shape: { fields: shapes, rest: undefined },
|
|
865
|
+
keys,
|
|
866
|
+
};
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
/** The keys any of the members holding `memberFields` holds. */
|
|
870
|
+
const keysOf = (
|
|
871
|
+
memberFields: ReadonlyArray<VariantSchema.Struct.Fields>,
|
|
872
|
+
): ReadonlyArray<string> => [...new Set(memberFields.flatMap((f) => Object.keys(f)))];
|
|
873
|
+
|
|
874
|
+
/** Whether a field is a struct, {@link FirestoreModel.Optional} or not. */
|
|
875
|
+
const isStructField = (field: unknown): boolean => VariantSchema.isStruct(unwrap(field));
|
|
876
|
+
|
|
877
|
+
/** The fields of the struct a field is, {@link FirestoreModel.Optional} or not. */
|
|
878
|
+
const structFields = (field: unknown): VariantSchema.Struct.Fields =>
|
|
879
|
+
fields(unwrap(field) as VariantSchema.Struct<any>);
|
|
880
|
+
|
|
881
|
+
/** Whether a field is a union, {@link FirestoreModel.Optional} or not. */
|
|
882
|
+
const isUnionField = (field: unknown): boolean => Predicate.hasProperty(unwrap(field), UnionTypeId);
|
|
883
|
+
|
|
884
|
+
/** The members of the union a field is, {@link FirestoreModel.Optional} or not. */
|
|
885
|
+
const unionMembers = (field: unknown): Members => (unwrap(field) as any)[UnionTypeId];
|
|
886
|
+
|
|
887
|
+
/** Whether an update reaches into a field: a struct, a Record or a union. */
|
|
888
|
+
const isNodeField = (field: unknown): boolean =>
|
|
889
|
+
isStructField(field) || isUnionField(field) || Predicate.hasProperty(unwrap(field), RecordTypeId);
|
|
890
|
+
|
|
891
|
+
/** The fields of every member of each of the unions `atKey`, in one list. */
|
|
892
|
+
const unionsMemberFields = (
|
|
893
|
+
atKey: ReadonlyArray<unknown>,
|
|
894
|
+
): ReadonlyArray<VariantSchema.Struct.Fields> =>
|
|
895
|
+
atKey.flatMap((field) => unionMembers(field).map(fields));
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* What each of the unions `atKey` is replaced with, where every one of them is
|
|
899
|
+
* replaced with something and some value is taken by all of them, as the type
|
|
900
|
+
* tells it.
|
|
901
|
+
*/
|
|
902
|
+
const sharedReplacement = (
|
|
903
|
+
atKey: ReadonlyArray<unknown>,
|
|
904
|
+
): ReadonlyArray<Schema.Top> | undefined => {
|
|
905
|
+
const creates: Array<Schema.Top> = [];
|
|
906
|
+
for (const field of atKey) {
|
|
907
|
+
const replaceable = unionMembers(field).filter(isMemberReplaceable);
|
|
908
|
+
if (replaceable.length === 0) {
|
|
909
|
+
return undefined;
|
|
910
|
+
}
|
|
911
|
+
creates.push(unionCreate(replaceable));
|
|
912
|
+
}
|
|
913
|
+
return haveValueInCommon(creates) ? creates : undefined;
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Builds what the unions `atKey` the members of a union hold at one key take
|
|
918
|
+
* in `mode`, gathering the paths it writes at into `paths`, or `undefined`
|
|
919
|
+
* where they take nothing. It is the rule a union field is written by, held to
|
|
920
|
+
* every one of them: a replacement every one takes and stores the same way,
|
|
921
|
+
* the removal where every one may be missing, and a patch at the Shared Fields
|
|
922
|
+
* of all their members together, refused where any of them may be missing and
|
|
923
|
+
* the patch is checked.
|
|
924
|
+
*/
|
|
925
|
+
const buildSharedUnion = (
|
|
926
|
+
atKey: ReadonlyArray<unknown>,
|
|
927
|
+
path: Path,
|
|
928
|
+
paths: Paths,
|
|
929
|
+
mode: FirestoreModel.UpdateMode,
|
|
930
|
+
): { readonly schema: Schema.Top; readonly shape: Shape } | undefined => {
|
|
931
|
+
const optional = atKey.every(isOptional);
|
|
932
|
+
const creates = sharedReplacement(atKey);
|
|
933
|
+
const shared = buildShared(unionsMemberFields(atKey), path, paths, mode);
|
|
934
|
+
const takes: Array<Schema.Top> = [];
|
|
935
|
+
if (shared.keys.length > 0 && (mode === "unchecked" || !atKey.some(isOptional))) {
|
|
936
|
+
takes.push(shared.patch);
|
|
937
|
+
}
|
|
938
|
+
if (creates !== undefined) {
|
|
939
|
+
takes.push(ReplaceSchema(creates));
|
|
940
|
+
}
|
|
941
|
+
if (optional) {
|
|
942
|
+
takes.push(firestoreSchema.DeleteField);
|
|
943
|
+
}
|
|
944
|
+
const whole = creates === undefined ? undefined : SharedValueSchema(creates);
|
|
945
|
+
const written = optional
|
|
946
|
+
? whole === undefined
|
|
947
|
+
? firestoreSchema.DeleteField
|
|
948
|
+
: firestoreSchema.DeleteFieldOr(whole)
|
|
949
|
+
: whole;
|
|
950
|
+
if (written !== undefined) {
|
|
951
|
+
addPath(paths, path, written);
|
|
952
|
+
}
|
|
953
|
+
return takes.length === 0
|
|
954
|
+
? undefined
|
|
955
|
+
: { schema: takes.length === 1 ? takes[0] : Schema.Union(takes), shape: shared.shape };
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Whether members holding `memberFields` take any key under `unchecked`, as
|
|
960
|
+
* {@link buildShared} would build it, but without building a schema.
|
|
961
|
+
*/
|
|
962
|
+
const hasSharedField = (memberFields: ReadonlyArray<VariantSchema.Struct.Fields>): boolean =>
|
|
963
|
+
keysOf(memberFields).some((key) => {
|
|
964
|
+
const atKey = memberFields.filter((f) => Object.hasOwn(f, key)).map((f) => f[key]);
|
|
965
|
+
if (atKey.every(isStructField)) {
|
|
966
|
+
return atKey.every((field) => !isOptional(field))
|
|
967
|
+
? hasSharedField(atKey.map(structFields))
|
|
968
|
+
: atKey.every(isOptional);
|
|
969
|
+
}
|
|
970
|
+
if (atKey.every(isUnionField)) {
|
|
971
|
+
return (
|
|
972
|
+
atKey.every(isOptional) ||
|
|
973
|
+
sharedReplacement(atKey) !== undefined ||
|
|
974
|
+
hasSharedField(unionsMemberFields(atKey))
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
if (atKey.every((field) => !isNodeField(field))) {
|
|
978
|
+
const updates = atKey.map(updateSchema);
|
|
979
|
+
return updates.every((u) => u !== undefined) && haveValueInCommon(updates);
|
|
980
|
+
}
|
|
981
|
+
return false;
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
/** Builds the patch of a Record at the keys only the data names. */
|
|
985
|
+
const buildRest = (
|
|
986
|
+
rest: Rest<unknown>,
|
|
987
|
+
path: Path,
|
|
988
|
+
paths: Paths,
|
|
989
|
+
mode: FirestoreModel.UpdateMode,
|
|
990
|
+
): { readonly patch: Schema.Top; readonly shape: Shape } => {
|
|
991
|
+
const entryPath = [...path, rest.key];
|
|
992
|
+
const node = nodeOf(rest.value, true);
|
|
993
|
+
let entry: Schema.Top;
|
|
994
|
+
let shape: Shape | undefined;
|
|
995
|
+
if (node !== undefined) {
|
|
996
|
+
const built = buildNode({ ...node, optional: true }, entryPath, paths, mode);
|
|
997
|
+
entry = built.schema;
|
|
998
|
+
shape = built.shape;
|
|
999
|
+
} else {
|
|
1000
|
+
entry = firestoreSchema.DeleteFieldOr(
|
|
1001
|
+
withoutConstructorDefault(withoutOptionalKey(updateSchema(rest.value)!)),
|
|
1002
|
+
);
|
|
1003
|
+
addPath(paths, entryPath, entry);
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
patch: Schema.Record(Schema.toType(rest.key) as Schema.Record.Key, entry),
|
|
1007
|
+
shape: { fields: {}, rest: { value: shape } },
|
|
1008
|
+
};
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Goes one level down where the shape says a struct or a Record sits, unless it
|
|
1013
|
+
* is being removed or replaced. It runs on the patch as the caller wrote it,
|
|
1014
|
+
* before anything is encoded, so the removal and the replacement are still
|
|
1015
|
+
* this library's to recognise.
|
|
1016
|
+
*
|
|
1017
|
+
* A replacement is written whole, as the struct it holds. A key of a Record
|
|
1018
|
+
* fails where no path could name it; the paths it builds are encoded by the
|
|
1019
|
+
* schema of each, keys and all.
|
|
1020
|
+
*/
|
|
1021
|
+
const flatten = (
|
|
1022
|
+
value: Record<string, unknown>,
|
|
1023
|
+
shape: Shape,
|
|
1024
|
+
prefix: string,
|
|
1025
|
+
out: Record<string, unknown>,
|
|
1026
|
+
options: SchemaAST.ParseOptions,
|
|
1027
|
+
): Effect.Effect<Record<string, unknown>, SchemaIssue.Issue> =>
|
|
1028
|
+
Effect.gen(function* () {
|
|
1029
|
+
for (const key of Object.keys(value)) {
|
|
1030
|
+
const inner = value[key];
|
|
1031
|
+
let child: Shape | undefined;
|
|
1032
|
+
if (shape.rest !== undefined) {
|
|
1033
|
+
const reason = unaddressable(key);
|
|
1034
|
+
if (reason !== undefined) {
|
|
1035
|
+
return yield* Effect.fail(
|
|
1036
|
+
new SchemaIssue.InvalidValue(
|
|
1037
|
+
{
|
|
1038
|
+
message: `Invalid Firestore field name "${key}" in "${prefix.slice(0, -1)}": it ${reason}`,
|
|
1039
|
+
},
|
|
1040
|
+
key,
|
|
1041
|
+
options,
|
|
1042
|
+
),
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
child = shape.rest.value;
|
|
1046
|
+
} else if (Object.hasOwn(shape.fields, key)) {
|
|
1047
|
+
child = shape.fields[key];
|
|
1048
|
+
}
|
|
1049
|
+
if (child === undefined || fieldValue.isDeleteField(inner)) {
|
|
1050
|
+
out[prefix + key] = inner;
|
|
1051
|
+
} else if (patch.isReplace(inner)) {
|
|
1052
|
+
out[prefix + key] = inner.value;
|
|
1053
|
+
} else {
|
|
1054
|
+
yield* flatten(inner as Record<string, unknown>, child, `${prefix}${key}.`, out, options);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return out;
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* The update of a document, out of the patch it takes, where the patch goes
|
|
1062
|
+
* one level down, and the paths it writes at.
|
|
1063
|
+
*/
|
|
1064
|
+
const updateOf = (patch: Schema.Top, shape: Shape, paths: Paths): Schema.Top => {
|
|
1065
|
+
const known = Schema.Struct(paths.known);
|
|
1066
|
+
const patterns = [...paths.patterns.values()];
|
|
1067
|
+
return (patterns.length === 0 ? known : Schema.StructWithRest(known, patterns as any)).pipe(
|
|
1068
|
+
Schema.decodeTo(Schema.toType(patch), {
|
|
1069
|
+
decode: SchemaGetter.forbidden(() => "An update is only ever written, never read back"),
|
|
1070
|
+
encode: SchemaGetter.transformEffect((value, options) =>
|
|
1071
|
+
flatten(value as Record<string, unknown>, shape, "", {}, options),
|
|
1072
|
+
),
|
|
1073
|
+
}),
|
|
1074
|
+
);
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
const update = (
|
|
1078
|
+
fields: VariantSchema.Struct.Fields,
|
|
1079
|
+
mode: FirestoreModel.UpdateMode,
|
|
1080
|
+
): Schema.Top => {
|
|
1081
|
+
const paths = emptyPaths();
|
|
1082
|
+
const { patch, shape } = build(fields, [], paths, mode);
|
|
1083
|
+
return updateOf(patch, shape, paths);
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* The update of a document that is a union, patched at its Shared Fields - and,
|
|
1088
|
+
* unchecked, at a key only some members hold - by the rule a union field is
|
|
1089
|
+
* patched by. It is never written whole: a document changes member through
|
|
1090
|
+
* `set`.
|
|
1091
|
+
*/
|
|
1092
|
+
const unionUpdate = (members: Members, mode: FirestoreModel.UpdateMode): Schema.Top => {
|
|
1093
|
+
const paths = emptyPaths();
|
|
1094
|
+
const { patch, shape } = buildShared(members.map(fields), [], paths, mode);
|
|
1095
|
+
return updateOf(patch, shape, paths);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
/** @internal */
|
|
1099
|
+
export const Class =
|
|
1100
|
+
(identifier: string) =>
|
|
1101
|
+
(
|
|
1102
|
+
fields: VariantSchema.Struct.Fields,
|
|
1103
|
+
annotations?: Schema.Annotations.Declaration<any, any>,
|
|
1104
|
+
): any => {
|
|
1105
|
+
const Base = Variants.Class<any>(identifier)(fields as any, annotations) as any;
|
|
1106
|
+
// `Variants.Class` defines each variant as a non-configurable property, so
|
|
1107
|
+
// the patch `update` is shadowed from a subclass rather than written over
|
|
1108
|
+
// it. A class built this way is extended by the caller anyway, which is
|
|
1109
|
+
// what `Self` is for, so this is one more link in a chain that already has
|
|
1110
|
+
// one.
|
|
1111
|
+
class Model extends Base {}
|
|
1112
|
+
Object.defineProperty(Model, "update", {
|
|
1113
|
+
value: update(fields, "checked").annotate({
|
|
1114
|
+
id: `${identifier}.update`,
|
|
1115
|
+
title: `${identifier}.update`,
|
|
1116
|
+
}),
|
|
1117
|
+
});
|
|
1118
|
+
Object.defineProperty(Model, "updateUnchecked", {
|
|
1119
|
+
value: update(fields, "unchecked").annotate({
|
|
1120
|
+
id: `${identifier}.updateUnchecked`,
|
|
1121
|
+
title: `${identifier}.updateUnchecked`,
|
|
1122
|
+
}),
|
|
1123
|
+
});
|
|
1124
|
+
return Model;
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
/** @internal */
|
|
1128
|
+
export const Immutable = <S extends Schema.Top>(schema: S): FirestoreModel.Immutable<S> =>
|
|
1129
|
+
Field({ read: schema, create: schema });
|
|
1130
|
+
|
|
1131
|
+
/** @internal */
|
|
1132
|
+
export const Timestamp = <S extends FirestoreSchema.TimeSchema>(
|
|
1133
|
+
schema: S,
|
|
1134
|
+
): FirestoreModel.Timestamp<S> =>
|
|
1135
|
+
Field({
|
|
1136
|
+
read: schema,
|
|
1137
|
+
create: firestoreSchema.ServerTimestampOr(schema),
|
|
1138
|
+
update: firestoreSchema.ServerTimestampOr(schema),
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
/** @internal */
|
|
1142
|
+
export const Number = <S extends Schema.Codec<unknown, number, unknown, unknown>>(
|
|
1143
|
+
schema: S,
|
|
1144
|
+
): FirestoreModel.Number<S> =>
|
|
1145
|
+
Field({
|
|
1146
|
+
read: schema,
|
|
1147
|
+
create: firestoreSchema.IncrementOr(schema),
|
|
1148
|
+
update: firestoreSchema.IncrementOr(schema),
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
/** @internal */
|
|
1152
|
+
export const Array = <S extends Schema.Top>(item: S): FirestoreModel.Array<S> =>
|
|
1153
|
+
Field({
|
|
1154
|
+
read: Schema.Array(item),
|
|
1155
|
+
create: Schema.Array(item),
|
|
1156
|
+
update: firestoreSchema.ArrayOr(item),
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
const optionalField = fieldEvolve({
|
|
1160
|
+
read: Schema.optionalKey,
|
|
1161
|
+
create: Schema.optionalKey,
|
|
1162
|
+
update: (schema: Schema.Top) => Schema.optionalKey(firestoreSchema.DeleteFieldOr(schema)),
|
|
1163
|
+
} as any) as (self: unknown) => unknown;
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* What a struct or a Record takes as a field of its own, which is what
|
|
1167
|
+
* `extract` hands back: the patches an unchecked update takes.
|
|
1168
|
+
*/
|
|
1169
|
+
const nodeUpdate = (node: Node): Schema.Top =>
|
|
1170
|
+
Schema.optionalKey(buildNode(node, [], emptyPaths(), "unchecked").schema);
|
|
1171
|
+
|
|
1172
|
+
/** @internal */
|
|
1173
|
+
export const Record = <K extends Schema.Record.Key, V extends FirestoreModel.Value>(
|
|
1174
|
+
key: K,
|
|
1175
|
+
value: V,
|
|
1176
|
+
): FirestoreModel.Record<K, V> => {
|
|
1177
|
+
const { rest } = recordKeys(key);
|
|
1178
|
+
const of = (variant: "read" | "create"): Schema.Top | undefined => {
|
|
1179
|
+
const schema = variantSchema(value, variant);
|
|
1180
|
+
// A key only the data names may be missing anyway, and Effect takes no
|
|
1181
|
+
// optional key at one.
|
|
1182
|
+
return schema && Schema.Record(key, rest ? withoutOptionalKey(schema) : schema);
|
|
1183
|
+
};
|
|
1184
|
+
const read = of("read");
|
|
1185
|
+
const create = of("create");
|
|
1186
|
+
const node = recordNode(key, value, () => create!, false);
|
|
1187
|
+
return Object.assign(
|
|
1188
|
+
Field({
|
|
1189
|
+
...(read && { read }),
|
|
1190
|
+
...(create && { create }),
|
|
1191
|
+
...(node.writable && { update: nodeUpdate(node) }),
|
|
1192
|
+
}),
|
|
1193
|
+
{ [RecordTypeId]: { key, value } },
|
|
1194
|
+
) as any;
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
/** @internal */
|
|
1198
|
+
export const Union = (members: Members): any => {
|
|
1199
|
+
// Each member as the default variant is read, which keeps a class a class.
|
|
1200
|
+
// Effect builds the same for its own union, though only it can extract
|
|
1201
|
+
// that, and its union defines the variants for good, so it is rebuilt here.
|
|
1202
|
+
const read = Schema.Union((Variants.Union(members) as Schema.Union<any>).members);
|
|
1203
|
+
const node = unionNode(members, false);
|
|
1204
|
+
const schemas = {
|
|
1205
|
+
read,
|
|
1206
|
+
create: node.create(),
|
|
1207
|
+
...(node.writable && { update: nodeUpdate(node) }),
|
|
1208
|
+
};
|
|
1209
|
+
return Object.defineProperties(read, {
|
|
1210
|
+
[FieldTypeId]: { value: FieldTypeId },
|
|
1211
|
+
schemas: { value: schemas },
|
|
1212
|
+
read: { value: Schema.Union(members.map((member) => extract(member, "read") as Schema.Top)) },
|
|
1213
|
+
create: { value: schemas.create },
|
|
1214
|
+
// What a document that is the union is updated through.
|
|
1215
|
+
update: { value: unionUpdate(members, "checked") },
|
|
1216
|
+
updateUnchecked: { value: unionUpdate(members, "unchecked") },
|
|
1217
|
+
[UnionTypeId]: { value: members },
|
|
1218
|
+
});
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const optionalNode = (self: unknown, node: Node): unknown =>
|
|
1222
|
+
Field({
|
|
1223
|
+
read: Schema.optionalKey(variantSchema(self, "read")!),
|
|
1224
|
+
create: Schema.optionalKey(node.create()),
|
|
1225
|
+
...(node.writable && { update: nodeUpdate(node) }),
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
/** @internal */
|
|
1229
|
+
export const Optional = (self: FirestoreModel.Value): any => {
|
|
1230
|
+
// Made optional already, which a second time leaves as it is.
|
|
1231
|
+
if (isOptional(self)) {
|
|
1232
|
+
return self;
|
|
1233
|
+
}
|
|
1234
|
+
const node = nodeOf(self, true);
|
|
1235
|
+
return Object.assign(
|
|
1236
|
+
(node === undefined ? optionalField(self) : optionalNode(self, node)) as object,
|
|
1237
|
+
{
|
|
1238
|
+
"~optionality": "optional",
|
|
1239
|
+
field: self,
|
|
1240
|
+
},
|
|
1241
|
+
);
|
|
1242
|
+
};
|