@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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/dist/_virtual/_rolldown/runtime.mjs +12 -0
  4. package/dist/app/App.d.mts +27 -0
  5. package/dist/app/App.mjs +27 -0
  6. package/dist/app/index.d.mts +2 -0
  7. package/dist/app/index.mjs +1 -0
  8. package/dist/firestore/Collection.d.mts +423 -0
  9. package/dist/firestore/Collection.mjs +133 -0
  10. package/dist/firestore/CollectionGroup.d.mts +164 -0
  11. package/dist/firestore/CollectionGroup.mjs +84 -0
  12. package/dist/firestore/DocumentPath.d.mts +95 -0
  13. package/dist/firestore/DocumentPath.mjs +71 -0
  14. package/dist/firestore/DocumentSnapshot.d.mts +239 -0
  15. package/dist/firestore/DocumentSnapshot.mjs +134 -0
  16. package/dist/firestore/FieldValue.d.mts +332 -0
  17. package/dist/firestore/FieldValue.mjs +256 -0
  18. package/dist/firestore/Firestore.d.mts +1165 -0
  19. package/dist/firestore/Firestore.mjs +274 -0
  20. package/dist/firestore/FirestoreModel.d.mts +1232 -0
  21. package/dist/firestore/FirestoreModel.mjs +586 -0
  22. package/dist/firestore/FirestoreSchema.d.mts +567 -0
  23. package/dist/firestore/FirestoreSchema.mjs +419 -0
  24. package/dist/firestore/FirestoreValue.d.mts +145 -0
  25. package/dist/firestore/FirestoreValue.mjs +87 -0
  26. package/dist/firestore/GeoPoint.d.mts +90 -0
  27. package/dist/firestore/GeoPoint.mjs +75 -0
  28. package/dist/firestore/Patch.d.mts +73 -0
  29. package/dist/firestore/Patch.mjs +57 -0
  30. package/dist/firestore/QuerySnapshot.d.mts +107 -0
  31. package/dist/firestore/QuerySnapshot.mjs +43 -0
  32. package/dist/firestore/Timestamp.d.mts +273 -0
  33. package/dist/firestore/Timestamp.mjs +255 -0
  34. package/dist/firestore/index.d.mts +14 -0
  35. package/dist/firestore/index.mjs +14 -0
  36. package/dist/firestore/internal/collection.mjs +156 -0
  37. package/dist/firestore/internal/collectionGroup.mjs +64 -0
  38. package/dist/firestore/internal/documentPath.d.mts +4 -0
  39. package/dist/firestore/internal/documentPath.mjs +42 -0
  40. package/dist/firestore/internal/documentSnapshot.mjs +63 -0
  41. package/dist/firestore/internal/fieldValue.d.mts +11 -0
  42. package/dist/firestore/internal/fieldValue.mjs +88 -0
  43. package/dist/firestore/internal/firestore.mjs +145 -0
  44. package/dist/firestore/internal/firestoreError.d.mts +29 -0
  45. package/dist/firestore/internal/firestoreError.mjs +30 -0
  46. package/dist/firestore/internal/firestoreModel.mjs +719 -0
  47. package/dist/firestore/internal/firestoreSchema.mjs +123 -0
  48. package/dist/firestore/internal/firestoreValue.mjs +108 -0
  49. package/dist/firestore/internal/firestoreValueWalk.d.mts +58 -0
  50. package/dist/firestore/internal/firestoreValueWalk.mjs +223 -0
  51. package/dist/firestore/internal/geoPoint.d.mts +4 -0
  52. package/dist/firestore/internal/geoPoint.mjs +39 -0
  53. package/dist/firestore/internal/patch.mjs +19 -0
  54. package/dist/firestore/internal/querySnapshot.mjs +24 -0
  55. package/dist/firestore/internal/schemaAST.mjs +35 -0
  56. package/dist/firestore/internal/snapshotExtra.mjs +35 -0
  57. package/dist/firestore/internal/streamFromListener.mjs +12 -0
  58. package/dist/firestore/internal/timestamp.d.mts +4 -0
  59. package/dist/firestore/internal/timestamp.mjs +66 -0
  60. package/dist/firestore/internal/timestampSchema.mjs +31 -0
  61. package/dist/firestore/internal/transactionFromCallback.mjs +66 -0
  62. package/dist/functions/Callable.d.mts +654 -0
  63. package/dist/functions/Callable.mjs +450 -0
  64. package/dist/functions/CallableError.d.mts +297 -0
  65. package/dist/functions/CallableError.mjs +256 -0
  66. package/dist/functions/CallableMiddleware.d.mts +213 -0
  67. package/dist/functions/CallableMiddleware.mjs +52 -0
  68. package/dist/functions/CallableRequest.d.mts +166 -0
  69. package/dist/functions/CallableRequest.mjs +18 -0
  70. package/dist/functions/Caller.d.mts +103 -0
  71. package/dist/functions/Caller.mjs +115 -0
  72. package/dist/functions/DocumentTrigger.d.mts +795 -0
  73. package/dist/functions/DocumentTrigger.mjs +566 -0
  74. package/dist/functions/FirebaseFunction.d.mts +79 -0
  75. package/dist/functions/FirebaseFunction.mjs +4 -0
  76. package/dist/functions/FunctionGroup.d.mts +175 -0
  77. package/dist/functions/FunctionGroup.mjs +64 -0
  78. package/dist/functions/index.d.mts +9 -0
  79. package/dist/functions/index.mjs +9 -0
  80. package/dist/functions/internal/callable.mjs +167 -0
  81. package/dist/functions/internal/callableMiddleware.d.mts +19 -0
  82. package/dist/functions/internal/callableMiddleware.mjs +15 -0
  83. package/dist/functions/internal/callableRequest.mjs +6 -0
  84. package/dist/functions/internal/documentTrigger.d.mts +10 -0
  85. package/dist/functions/internal/documentTrigger.mjs +73 -0
  86. package/dist/functions/internal/firebaseFunction.d.mts +5 -0
  87. package/dist/functions/internal/firebaseFunction.mjs +24 -0
  88. package/dist/functions/internal/functionGroup.mjs +36 -0
  89. package/package.json +72 -3
  90. package/src/app/App.ts +24 -0
  91. package/src/app/index.ts +7 -0
  92. package/src/firestore/Collection.ts +520 -0
  93. package/src/firestore/CollectionGroup.ts +245 -0
  94. package/src/firestore/DocumentPath.ts +114 -0
  95. package/src/firestore/DocumentSnapshot.ts +308 -0
  96. package/src/firestore/FieldValue.ts +375 -0
  97. package/src/firestore/Firestore.ts +1432 -0
  98. package/src/firestore/FirestoreModel.ts +1884 -0
  99. package/src/firestore/FirestoreSchema.ts +643 -0
  100. package/src/firestore/FirestoreValue.ts +207 -0
  101. package/src/firestore/GeoPoint.ts +105 -0
  102. package/src/firestore/Patch.ts +117 -0
  103. package/src/firestore/QuerySnapshot.ts +150 -0
  104. package/src/firestore/Timestamp.ts +302 -0
  105. package/src/firestore/index.ts +67 -0
  106. package/src/firestore/internal/collection.ts +246 -0
  107. package/src/firestore/internal/collectionGroup.ts +81 -0
  108. package/src/firestore/internal/documentPath.ts +53 -0
  109. package/src/firestore/internal/documentSnapshot.ts +117 -0
  110. package/src/firestore/internal/fieldValue.ts +119 -0
  111. package/src/firestore/internal/firestore.ts +322 -0
  112. package/src/firestore/internal/firestoreError.ts +36 -0
  113. package/src/firestore/internal/firestoreModel.ts +1242 -0
  114. package/src/firestore/internal/firestoreSchema.ts +218 -0
  115. package/src/firestore/internal/firestoreValue.ts +202 -0
  116. package/src/firestore/internal/firestoreValueWalk.ts +349 -0
  117. package/src/firestore/internal/geoPoint.ts +59 -0
  118. package/src/firestore/internal/patch.ts +21 -0
  119. package/src/firestore/internal/querySnapshot.ts +39 -0
  120. package/src/firestore/internal/schemaAST.ts +83 -0
  121. package/src/firestore/internal/snapshotExtra.ts +47 -0
  122. package/src/firestore/internal/streamFromListener.ts +27 -0
  123. package/src/firestore/internal/timestamp.ts +119 -0
  124. package/src/firestore/internal/timestampSchema.ts +42 -0
  125. package/src/firestore/internal/transactionFromCallback.ts +106 -0
  126. package/src/functions/Callable.ts +855 -0
  127. package/src/functions/CallableError.ts +348 -0
  128. package/src/functions/CallableMiddleware.ts +279 -0
  129. package/src/functions/CallableRequest.ts +207 -0
  130. package/src/functions/Caller.ts +141 -0
  131. package/src/functions/DocumentTrigger.ts +927 -0
  132. package/src/functions/FirebaseFunction.ts +96 -0
  133. package/src/functions/FunctionGroup.ts +225 -0
  134. package/src/functions/index.ts +62 -0
  135. package/src/functions/internal/callable.ts +272 -0
  136. package/src/functions/internal/callableMiddleware.ts +54 -0
  137. package/src/functions/internal/callableRequest.ts +8 -0
  138. package/src/functions/internal/documentTrigger.ts +143 -0
  139. package/src/functions/internal/firebaseFunction.ts +58 -0
  140. package/src/functions/internal/functionGroup.ts +47 -0
@@ -0,0 +1,719 @@
1
+ import { isDeleteField } from "./fieldValue.mjs";
2
+ import { equals } from "./firestoreValueWalk.mjs";
3
+ import { toCodecFirestore } from "./firestoreValue.mjs";
4
+ import { ArrayOr, DeleteField, DeleteFieldOr, IncrementOr, ServerTimestampOr } from "./firestoreSchema.mjs";
5
+ import { isReplace, replace } from "./patch.mjs";
6
+ import { Effect, Predicate, Result, Schema, SchemaAST, SchemaGetter, SchemaIssue, SchemaParser } from "effect";
7
+ import { VariantSchema } from "effect/schema";
8
+ // src/firestore/internal/firestoreModel.ts
9
+ const Variants = VariantSchema.make({
10
+ variants: [
11
+ "read",
12
+ "create",
13
+ "update"
14
+ ],
15
+ defaultVariant: "read"
16
+ });
17
+ /** @internal */
18
+ const Field = Variants.Field;
19
+ /** @internal */
20
+ const FieldOnly = Variants.FieldOnly;
21
+ /** @internal */
22
+ const FieldExcept = Variants.FieldExcept;
23
+ /** @internal */
24
+ const Struct$1 = Variants.Struct;
25
+ /** @internal */
26
+ const fieldEvolve = Variants.fieldEvolve;
27
+ /** @internal */
28
+ const extract = Variants.extract;
29
+ /** @internal */
30
+ const UnionTypeId = "~@visiolab/effect-firebase/firestore/FirestoreModel/Union";
31
+ const FieldTypeId = "~effect/schema/VariantSchema/Field";
32
+ /** @internal */
33
+ const fields = (self) => VariantSchema.fields(self);
34
+ /** @internal */
35
+ const RecordTypeId = "~@visiolab/effect-firebase/firestore/FirestoreModel/Record";
36
+ /** No path yet, to gather an update's into. */
37
+ const emptyPaths = () => ({
38
+ known: {},
39
+ patterns: /* @__PURE__ */ new Map()
40
+ });
41
+ /**
42
+ * Throws on a field name holding a dot, which a path would read as two.
43
+ */
44
+ const assertFieldName = (key) => {
45
+ if (key.includes(".")) throw new Error(`Invalid Firestore field name "${key}": it must not contain "."`);
46
+ };
47
+ /** The reason a key is not one a path can name on its own, if it is not. */
48
+ const unaddressable = (key) => key === "" ? "must not be empty" : key.includes(".") ? "must not contain \".\"" : void 0;
49
+ /** A key a path names on its own, which is a non-empty one holding no dot. */
50
+ const Segment = Schema.String.check(Schema.isPattern(/^[^.]+$/));
51
+ /** Where a path sits, with `*` for each key only the data names. */
52
+ const where = (path) => path.map((segment) => Schema.isSchema(segment) ? "*" : segment).join(".");
53
+ /**
54
+ * The parts of the template literal a path matches: the names of the fields
55
+ * along it, joined by dots, and `key` for each key of a Record.
56
+ */
57
+ const template = (path, key) => {
58
+ const parts = [];
59
+ let literal = "";
60
+ path.forEach((segment, i) => {
61
+ if (i > 0) literal += ".";
62
+ if (Schema.isSchema(segment)) {
63
+ if (literal !== "") parts.push(literal);
64
+ parts.push(key(segment, i));
65
+ literal = "";
66
+ } else literal += segment;
67
+ });
68
+ if (literal !== "") parts.push(literal);
69
+ return parts;
70
+ };
71
+ /**
72
+ * The key of a pattern of paths, as an update is written with it and as
73
+ * Firestore holds it: each key of a Record is encoded by the Record's key
74
+ * schema, and refused where its encoded form is one no path can name.
75
+ */
76
+ const PathKey = (path) => {
77
+ const encoded = template(path, (key, i) => Schema.String.check(Schema.makeFilter((segment) => {
78
+ const reason = unaddressable(segment);
79
+ return reason === void 0 ? void 0 : `Invalid Firestore field name "${segment}" in "${where(path.slice(0, i))}": it ${reason}`;
80
+ })).pipe(Schema.decodeTo(key)));
81
+ const decoded = template(path, (key) => recordKeys(key).rest ? Segment : Schema.toType(key));
82
+ return Schema.TemplateLiteralParser(encoded).pipe(Schema.decodeTo(Schema.TemplateLiteral(decoded), {
83
+ decode: SchemaGetter.transform((parts) => parts.join("")),
84
+ encode: SchemaGetter.transform((key) => {
85
+ const segments = key.split(".");
86
+ return template(path, (_, i) => segments[i]);
87
+ })
88
+ }));
89
+ };
90
+ const addPath = (paths, path, schema) => {
91
+ if (path.every((segment) => typeof segment === "string")) paths.known[path.join(".")] = Schema.optionalKey(schema);
92
+ else if (!paths.patterns.has(where(path))) paths.patterns.set(where(path), Schema.Record(PathKey(path), withoutOptionalKey(schema)));
93
+ };
94
+ const variantSchema = (field, variant) => VariantSchema.isStruct(field) ? extract(field, variant) : VariantSchema.isField(field) ? field.schemas[variant] : field;
95
+ const updateSchema = (field) => VariantSchema.isStruct(field) ? void 0 : variantSchema(field, "update");
96
+ const replaceContext = SchemaAST.replaceContext;
97
+ /**
98
+ * Drops the default a field is constructed with. A patch leaves a key out to
99
+ * keep what the document holds, so filling one in would overwrite it.
100
+ */
101
+ const withoutConstructorDefault = (schema) => {
102
+ const context = schema.ast.context;
103
+ if (context?.constructorDefault === void 0) return schema;
104
+ return Schema.make(replaceContext(schema.ast, new SchemaAST.Context(context.isOptional, context.isMutable, void 0, context.annotations)));
105
+ };
106
+ /**
107
+ * Drops the optional key a schema is made, for a value at a key only the data
108
+ * names, which may be missing whatever it holds.
109
+ */
110
+ const withoutOptionalKey = (schema) => {
111
+ const context = schema.ast.context;
112
+ if (context?.isOptional !== true) return schema;
113
+ return Schema.make(replaceContext(schema.ast, new SchemaAST.Context(false, context.isMutable, context.constructorDefault, context.annotations)));
114
+ };
115
+ /** The keys a Record key schema names up front, and whether the data names more. */
116
+ const recordKeys = (key) => {
117
+ const ast = Schema.Record(key, Schema.Unknown).ast;
118
+ const known = ast.propertySignatures.map(({ name }) => {
119
+ if (typeof name === "symbol") throw new Error(`Invalid Firestore field name ${String(name)}: it must be a string`);
120
+ const segment = String(name);
121
+ const reason = unaddressable(segment);
122
+ if (reason !== void 0) throw new Error(`Invalid Firestore field name "${segment}": it ${reason}`);
123
+ return segment;
124
+ });
125
+ const rest = ast.indexSignatures.length > 0;
126
+ if (rest && known.length > 0) throw new Error(`Invalid Record key: it names ${known.map((k) => `"${k}"`).join(", ")} besides keys only the data names, which an update cannot tell apart`);
127
+ return {
128
+ known,
129
+ rest
130
+ };
131
+ };
132
+ const recordNode = (key, value, create, optional) => {
133
+ const keys = recordKeys(key);
134
+ return {
135
+ fields: Object.fromEntries(keys.known.map((k) => [k, value])),
136
+ key: keys.rest ? void 0 : key,
137
+ rest: keys.rest ? {
138
+ key,
139
+ value
140
+ } : void 0,
141
+ create,
142
+ optional,
143
+ writable: nodeOf(value)?.writable ?? updateSchema(value) !== void 0
144
+ };
145
+ };
146
+ /** Whether a field was made {@link FirestoreModel.Optional}. */
147
+ const isOptional = (field) => Predicate.hasProperty(field, "~optionality") && field["~optionality"] === "optional";
148
+ /** What a field reaches into, if it is a struct or a Record. */
149
+ const nodeOf = (field, optional = false) => {
150
+ if (isOptional(field)) return nodeOf(field.field, true);
151
+ if (Predicate.hasProperty(field, "~@visiolab/effect-firebase/firestore/FirestoreModel/Union")) return unionNode(field[UnionTypeId], optional);
152
+ if (Predicate.hasProperty(field, "~@visiolab/effect-firebase/firestore/FirestoreModel/Record")) {
153
+ const { key, value } = field[RecordTypeId];
154
+ return recordNode(key, value, () => variantSchema(field, "create"), optional);
155
+ }
156
+ if (VariantSchema.isStruct(field)) return {
157
+ fields: VariantSchema.fields(field),
158
+ key: void 0,
159
+ rest: void 0,
160
+ create: () => extract(field, "create"),
161
+ optional,
162
+ writable: true
163
+ };
164
+ };
165
+ /**
166
+ * Whether a set of fields may be written whole by an update: only where every
167
+ * field it holds, at any depth, is written both by `set`, whose shape a
168
+ * replacement takes, and by an update. Replacing one holding a field written
169
+ * once would change it, and one holding a field `set` leaves out would drop it.
170
+ */
171
+ const isReplaceable = (fields) => Object.values(fields).every((field) => {
172
+ const node = nodeOf(field);
173
+ return node === void 0 ? variantSchema(field, "create") !== void 0 && updateSchema(field) !== void 0 : isNodeReplaceable(node);
174
+ });
175
+ const isNodeReplaceable = (node) => node.members !== void 0 ? node.members.every(isMemberReplaceable) : isReplaceable(node.fields) && (node.rest === void 0 || isReplaceable({ _: node.rest.value }));
176
+ const isMemberReplaceable = (member) => isReplaceable(fields(member));
177
+ /** The union of what `set` writes each of `members` as. */
178
+ const unionCreate = (members) => Schema.Union(members.map((member) => extract(member, "create")));
179
+ const unionNode = (members, optional) => ({
180
+ fields: {},
181
+ members,
182
+ key: void 0,
183
+ rest: void 0,
184
+ create: () => unionCreate(members),
185
+ optional,
186
+ writable: optional || members.some(isMemberReplaceable) || hasSharedField(members.map(fields))
187
+ });
188
+ /**
189
+ * The replacement of a struct, holding the struct as `create` makes it - with
190
+ * its constructor defaults filled in, since a default left out of a struct
191
+ * written whole would be a field dropped.
192
+ *
193
+ * Given several, as the unions the members of a union hold at one key are, it
194
+ * is made by every one of them, each of which must take it, and holds it as
195
+ * the first makes it. That they also store it the same way is for the schema
196
+ * of its path to tell - see {@link SharedValueSchema}.
197
+ */
198
+ const ReplaceSchema = (creates) => Schema.declareConstructor()(creates, (creates) => (input, ast, options) => {
199
+ if (!isReplace(input)) return Effect.fail(new SchemaIssue.InvalidType(ast, input, options));
200
+ return Effect.forEach(creates, (create) => create.makeEffect(input.value, options)).pipe(Effect.mapBothEager({
201
+ onSuccess: ([made]) => replace(made),
202
+ onFailure: (issue) => new SchemaIssue.Pointer(["value"], issue)
203
+ }));
204
+ }, { expected: "a struct written whole" });
205
+ /**
206
+ * The patch of a struct, refusing the markers a `Schema.Struct` would
207
+ * otherwise take for a patch of nothing, since it keeps only the keys it knows.
208
+ */
209
+ const StructPatchSchema = (fields) => Schema.declareConstructor()([fields], ([fields]) => (input, ast, options) => isReplace(input) || isDeleteField(input) ? Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) : SchemaParser.decodeUnknownEffect(fields)(input, options), { expected: "a patch of a struct" });
210
+ /**
211
+ * Builds the patch a set of fields is updated through in `mode`, along with
212
+ * where its structs and Records sit, and gathers the schema of each path it can
213
+ * write at into `paths`.
214
+ *
215
+ * A struct takes a patch, written at its paths, unless it may be missing and
216
+ * the patch is checked, since the patch would create it holding only what it
217
+ * carried. It takes a replacement wherever it is replaceable, and a removal
218
+ * where it may be missing. A Record is a struct whose keys may come from the
219
+ * data - see {@link buildNode}.
220
+ *
221
+ * Where `recordKey` is given, the fields are the keys of a Record keyed by
222
+ * literals, and each sits on its path as that key schema encodes it.
223
+ *
224
+ * Throws on a field name holding a dot, which a path would read as two.
225
+ */
226
+ const build = (fields, path, paths, mode, recordKey) => {
227
+ const schemas = {};
228
+ const shapes = {};
229
+ for (const key of Object.keys(fields)) {
230
+ assertFieldName(key);
231
+ const field = fields[key];
232
+ const at = [...path, recordKey ?? key];
233
+ const node = nodeOf(field);
234
+ if (node !== void 0) {
235
+ if (node.writable) {
236
+ const built = buildNode(node, at, paths, mode);
237
+ schemas[key] = Schema.optionalKey(built.schema);
238
+ shapes[key] = built.shape;
239
+ }
240
+ } else {
241
+ const update = updateSchema(field);
242
+ if (update !== void 0) {
243
+ const schema = withoutConstructorDefault(update);
244
+ schemas[key] = Schema.optionalKey(schema);
245
+ addPath(paths, at, schema);
246
+ }
247
+ }
248
+ }
249
+ return {
250
+ patch: Schema.Struct(schemas),
251
+ shape: {
252
+ fields: shapes,
253
+ rest: void 0
254
+ }
255
+ };
256
+ };
257
+ /**
258
+ * Builds what a struct or a Record at `path` takes in `mode`, gathering the
259
+ * paths it writes at into `paths`, the way {@link build} does for a set of
260
+ * fields.
261
+ *
262
+ * A Record whose keys come from the data is patched key by key, each key
263
+ * written at its own path. Any such key may be missing, so each takes the
264
+ * removal, and a value that is itself a struct or a Record is treated as one a
265
+ * document may go without. That also makes a patch safe where the Record
266
+ * itself may be missing: every key it writes is one the Record may hold or not.
267
+ */
268
+ const buildNode = (node, path, paths, mode) => {
269
+ if (node.members !== void 0) return buildUnion(node, node.members, path, paths, mode);
270
+ const inner = node.rest === void 0 ? build(node.fields, path, paths, mode, node.key) : buildRest(node.rest, path, paths, mode);
271
+ const create = isNodeReplaceable(node) ? node.create() : void 0;
272
+ const takes = [];
273
+ if (!node.optional || mode === "unchecked" || node.rest !== void 0) takes.push(StructPatchSchema(inner.patch));
274
+ if (create !== void 0) takes.push(ReplaceSchema([create]));
275
+ if (node.optional) takes.push(DeleteField);
276
+ const written = node.optional ? create === void 0 ? DeleteField : DeleteFieldOr(create) : create;
277
+ if (written !== void 0) addPath(paths, path, written);
278
+ return {
279
+ schema: takes.length === 1 ? takes[0] : Schema.Union(takes),
280
+ shape: inner.shape
281
+ };
282
+ };
283
+ /**
284
+ * Builds what a union at `path` takes in `mode`, gathering the paths it writes
285
+ * at into `paths`. A union is replaced with any member it may be replaced
286
+ * with, removed where it may be missing, and patched at its Shared Fields -
287
+ * and, unchecked, at a key only some members hold - see {@link buildShared}.
288
+ * The patch is refused where it may be missing and the patch is checked, as it
289
+ * is for a struct. One that takes none of these in `mode` takes nothing.
290
+ */
291
+ const buildUnion = (node, members, path, paths, mode) => {
292
+ const replaceable = members.filter(isMemberReplaceable);
293
+ const create = replaceable.length === 0 ? void 0 : unionCreate(replaceable);
294
+ const shared = buildShared(members.map(fields), path, paths, mode);
295
+ const takes = [];
296
+ if (shared.keys.length > 0 && (!node.optional || mode === "unchecked")) takes.push(shared.patch);
297
+ if (create !== void 0) takes.push(ReplaceSchema([create]));
298
+ if (node.optional) takes.push(DeleteField);
299
+ const written = node.optional ? create === void 0 ? DeleteField : DeleteFieldOr(create) : create;
300
+ if (written !== void 0) addPath(paths, path, written);
301
+ return {
302
+ schema: takes.length === 1 ? takes[0] : Schema.Union(takes),
303
+ shape: shared.shape
304
+ };
305
+ };
306
+ /** What a field was made {@link FirestoreModel.Optional} of, or the field itself. */
307
+ const unwrap = (field) => isOptional(field) ? field.field : field;
308
+ /** The types a type is a union of, or the type itself. */
309
+ const constituents = (ast) => SchemaAST.isUnion(ast) ? ast.types.flatMap(constituents) : [ast];
310
+ /** What a declaration is told apart by, where it says. */
311
+ const declarationId = (ast) => (ast.annotations?.representation)?.id;
312
+ /** The type of a literal a keyword is the type of. */
313
+ const keywordOf = (literal) => typeof literal === "string" ? "String" : typeof literal === "number" ? "Number" : typeof literal === "boolean" ? "Boolean" : "BigInt";
314
+ /**
315
+ * Whether every value of `a` is one of `b`, the way TypeScript tells whether
316
+ * one type extends another, as far as the schemas say. Where they do not say -
317
+ * a declaration with nothing to tell it apart by, a suspended schema - it is
318
+ * taken to be, so the encoding decides.
319
+ */
320
+ const isSubtype = (a, b) => {
321
+ if (SchemaAST.isUnion(a)) return a.types.every((type) => isSubtype(type, b));
322
+ if (SchemaAST.isUnion(b)) return b.types.some((type) => isSubtype(a, type));
323
+ if (SchemaAST.isAny(b) || SchemaAST.isUnknown(b) || SchemaAST.isNever(a) || SchemaAST.isAny(a)) return true;
324
+ if (SchemaAST.isSuspend(a) || SchemaAST.isSuspend(b)) return true;
325
+ if (SchemaAST.isLiteral(a)) return SchemaAST.isLiteral(b) ? a.literal === b.literal : SchemaAST.isEnum(b) ? b.enums.some(([, value]) => value === a.literal) : SchemaAST.isTemplateLiteral(b) ? typeof a.literal === "string" : b._tag === keywordOf(a.literal);
326
+ if (SchemaAST.isEnum(a)) return a.enums.every(([, value]) => isSubtype(new SchemaAST.Literal(value), b));
327
+ if (SchemaAST.isTemplateLiteral(a)) return SchemaAST.isString(b) || SchemaAST.isTemplateLiteral(b);
328
+ if (SchemaAST.isUniqueSymbol(a)) return SchemaAST.isSymbol(b) || SchemaAST.isUniqueSymbol(b) && a.symbol === b.symbol;
329
+ if (SchemaAST.isUndefined(a) && SchemaAST.isVoid(b)) return true;
330
+ if (SchemaAST.isObjectKeyword(b)) return SchemaAST.isObjectKeyword(a) || SchemaAST.isObjects(a) || SchemaAST.isArrays(a) || SchemaAST.isDeclaration(a);
331
+ if (SchemaAST.isDeclaration(a)) {
332
+ if (SchemaAST.isDeclaration(b)) {
333
+ const [ida, idb] = [declarationId(a), declarationId(b)];
334
+ return a === b || ida === void 0 || idb === void 0 || ida === idb && a.typeParameters.length === b.typeParameters.length && a.typeParameters.every((p, i) => isSubtype(p, b.typeParameters[i]));
335
+ }
336
+ return SchemaAST.isObjects(b);
337
+ }
338
+ if (SchemaAST.isObjects(a)) {
339
+ if (SchemaAST.isDeclaration(b)) return declarationId(b) === void 0;
340
+ if (!SchemaAST.isObjects(b)) return false;
341
+ const own = new Map(a.propertySignatures.map((p) => [p.name, p.type]));
342
+ return b.propertySignatures.every((p) => {
343
+ const type = own.get(p.name);
344
+ return type === void 0 ? SchemaAST.isOptional(p.type) : (SchemaAST.isOptional(p.type) || !SchemaAST.isOptional(type)) && isSubtype(type, p.type);
345
+ }) && b.indexSignatures.every((index) => [...a.propertySignatures, ...a.indexSignatures].every((p) => isSubtype(p.type, index.type)));
346
+ }
347
+ if (SchemaAST.isArrays(a)) {
348
+ if (!SchemaAST.isArrays(b)) return false;
349
+ if (b.elements.length === 0 && b.rest.length === 1) return [...a.elements, ...a.rest].every((type) => isSubtype(type, b.rest[0]));
350
+ if (a.rest.length === 0 && b.rest.length === 0) return a.elements.length <= b.elements.length && b.elements.every((type, i) => {
351
+ const own = a.elements[i];
352
+ return own === void 0 ? SchemaAST.isOptional(type) : (SchemaAST.isOptional(type) || !SchemaAST.isOptional(own)) && isSubtype(own, type);
353
+ });
354
+ return a.elements.length === b.elements.length && a.rest.length === b.rest.length && [...a.elements, ...a.rest].every((type, i) => isSubtype(type, [...b.elements, ...b.rest][i]));
355
+ }
356
+ return a._tag === b._tag;
357
+ };
358
+ /**
359
+ * The values out of `a` and `b` that the other takes too: each type `a` is a
360
+ * union of that `b` takes whole, and the other way round, as `Meet` works it
361
+ * out in the types.
362
+ */
363
+ const meet = (a, b) => {
364
+ const within = (types) => (type) => types.some((other) => isSubtype(type, other));
365
+ return [...a.filter(within(b)), ...b.filter(within(a))];
366
+ };
367
+ /**
368
+ * Whether some value is taken by every one of `schemas`, as `MeetAll` tells it
369
+ * in the types, so a key is shared, and has a path, exactly where its type
370
+ * says. The literal that tells members apart takes none.
371
+ */
372
+ const haveValueInCommon = (schemas) => schemas.map((schema) => constituents(schema.ast)).reduceRight((acc, types) => meet(types, acc)).length > 0;
373
+ /**
374
+ * What a Shared Field is patched through: the value is encoded through the
375
+ * field of every member, each of which must take it, and all of which must
376
+ * store it as the same Firestore Value. Each member decides by the canonical
377
+ * codec, not by the encoding the caller asks for, since that is what Firestore
378
+ * ends up holding. It is written as the first member writes it, which is then
379
+ * what every member would write.
380
+ */
381
+ const SharedValueSchema = (schemas) => {
382
+ if (schemas.length === 1) return schemas[0];
383
+ const codecs = schemas.map((schema) => toCodecFirestore(schema));
384
+ return schemas[0].check(Schema.makeFilter((value, _, options) => {
385
+ let stored;
386
+ for (const [i, codec] of codecs.entries()) {
387
+ const result = SchemaParser.encodeUnknownResult(codec)(value, options);
388
+ if (Result.isFailure(result)) return result.failure;
389
+ if (i === 0) stored = result.success;
390
+ else if (!equals(stored, result.success)) return "Expected a value every member of the union stores the same way";
391
+ }
392
+ }, { expected: "a value every member of the union takes and stores the same way" }));
393
+ };
394
+ /**
395
+ * The patch of the Shared Fields of a union, refusing a key it does not know,
396
+ * since a `Schema.Struct` would otherwise drop it and write nothing. That is a
397
+ * key some member goes without, or one the members cannot agree on, such as
398
+ * the literal they are told apart by.
399
+ */
400
+ const SharedPatchSchema = (fields, keys) => Schema.declareConstructor()([fields], ([fields]) => (input, ast, options) => {
401
+ if (isReplace(input) || isDeleteField(input)) return Effect.fail(new SchemaIssue.InvalidType(ast, input, options));
402
+ if (Predicate.isObject(input)) {
403
+ const unknown = Object.keys(input).find((key) => !keys.includes(key));
404
+ if (unknown !== void 0) return Effect.fail(new SchemaIssue.Pointer([unknown], new SchemaIssue.UnexpectedKey(ast, input[unknown])));
405
+ }
406
+ return SchemaParser.decodeUnknownEffect(fields)(input, options);
407
+ }, { expected: "a patch of the Shared Fields of a union" });
408
+ /**
409
+ * Builds the patch of the Shared Fields of a union, given the fields of each
410
+ * of its members, along with where the structs among them sit, and gathers the
411
+ * schema of each path it can write at into `paths`.
412
+ *
413
+ * A Shared Field is a key every member holds, at which every member takes an
414
+ * update: a value, patched through {@link SharedValueSchema}, or a struct,
415
+ * patched at its own Shared Fields by the same rule one level down. A struct
416
+ * every member may go without takes the removal alone, and one only some
417
+ * members may go without takes nothing. A union is shared by the rule a union
418
+ * field is written by - see {@link buildSharedUnion}. A Record is not shared.
419
+ * Where no value is taken by every member's type, as
420
+ * {@link haveValueInCommon} tells, the key is not shared either, which is what
421
+ * keeps it out of `paths` exactly where the type keeps it out.
422
+ *
423
+ * Under `unchecked`, a key only some members hold is patched by the same rule
424
+ * over just those members. Its path is gathered in either mode, so the paths
425
+ * are the same in both and only the patch tells them apart.
426
+ *
427
+ * {@link hasSharedField} tells, without building any of it, whether this
428
+ * takes any key under `unchecked`, and must agree with it.
429
+ */
430
+ const buildShared = (memberFields, path, paths, mode) => {
431
+ const schemas = {};
432
+ const shapes = {};
433
+ for (const key of keysOf(memberFields)) {
434
+ assertFieldName(key);
435
+ const holding = memberFields.filter((f) => Object.hasOwn(f, key));
436
+ const taken = holding.length === memberFields.length || mode === "unchecked";
437
+ const at = [...path, key];
438
+ const atKey = holding.map((f) => f[key]);
439
+ if (atKey.every(isStructField)) {
440
+ if (atKey.every((field) => !isOptional(field))) {
441
+ const inner = buildShared(atKey.map(structFields), at, paths, mode);
442
+ if (taken && inner.keys.length > 0) {
443
+ schemas[key] = Schema.optionalKey(inner.patch);
444
+ shapes[key] = inner.shape;
445
+ }
446
+ } else if (atKey.every(isOptional)) {
447
+ if (taken) schemas[key] = Schema.optionalKey(DeleteField);
448
+ addPath(paths, at, DeleteField);
449
+ }
450
+ } else if (atKey.every(isUnionField)) {
451
+ const built = buildSharedUnion(atKey, at, paths, mode);
452
+ if (taken && built !== void 0) {
453
+ schemas[key] = Schema.optionalKey(built.schema);
454
+ shapes[key] = built.shape;
455
+ }
456
+ } else if (atKey.every((field) => !isNodeField(field))) {
457
+ const updates = atKey.map(updateSchema);
458
+ if (updates.every((u) => u !== void 0) && haveValueInCommon(updates)) {
459
+ const schema = SharedValueSchema(updates.map(withoutConstructorDefault));
460
+ if (taken) schemas[key] = Schema.optionalKey(schema);
461
+ addPath(paths, at, schema);
462
+ }
463
+ }
464
+ }
465
+ const keys = Object.keys(schemas);
466
+ return {
467
+ patch: SharedPatchSchema(Schema.Struct(schemas), keys),
468
+ shape: {
469
+ fields: shapes,
470
+ rest: void 0
471
+ },
472
+ keys
473
+ };
474
+ };
475
+ /** The keys any of the members holding `memberFields` holds. */
476
+ const keysOf = (memberFields) => [...new Set(memberFields.flatMap((f) => Object.keys(f)))];
477
+ /** Whether a field is a struct, {@link FirestoreModel.Optional} or not. */
478
+ const isStructField = (field) => VariantSchema.isStruct(unwrap(field));
479
+ /** The fields of the struct a field is, {@link FirestoreModel.Optional} or not. */
480
+ const structFields = (field) => fields(unwrap(field));
481
+ /** Whether a field is a union, {@link FirestoreModel.Optional} or not. */
482
+ const isUnionField = (field) => Predicate.hasProperty(unwrap(field), UnionTypeId);
483
+ /** The members of the union a field is, {@link FirestoreModel.Optional} or not. */
484
+ const unionMembers = (field) => unwrap(field)[UnionTypeId];
485
+ /** Whether an update reaches into a field: a struct, a Record or a union. */
486
+ const isNodeField = (field) => isStructField(field) || isUnionField(field) || Predicate.hasProperty(unwrap(field), "~@visiolab/effect-firebase/firestore/FirestoreModel/Record");
487
+ /** The fields of every member of each of the unions `atKey`, in one list. */
488
+ const unionsMemberFields = (atKey) => atKey.flatMap((field) => unionMembers(field).map(fields));
489
+ /**
490
+ * What each of the unions `atKey` is replaced with, where every one of them is
491
+ * replaced with something and some value is taken by all of them, as the type
492
+ * tells it.
493
+ */
494
+ const sharedReplacement = (atKey) => {
495
+ const creates = [];
496
+ for (const field of atKey) {
497
+ const replaceable = unionMembers(field).filter(isMemberReplaceable);
498
+ if (replaceable.length === 0) return;
499
+ creates.push(unionCreate(replaceable));
500
+ }
501
+ return haveValueInCommon(creates) ? creates : void 0;
502
+ };
503
+ /**
504
+ * Builds what the unions `atKey` the members of a union hold at one key take
505
+ * in `mode`, gathering the paths it writes at into `paths`, or `undefined`
506
+ * where they take nothing. It is the rule a union field is written by, held to
507
+ * every one of them: a replacement every one takes and stores the same way,
508
+ * the removal where every one may be missing, and a patch at the Shared Fields
509
+ * of all their members together, refused where any of them may be missing and
510
+ * the patch is checked.
511
+ */
512
+ const buildSharedUnion = (atKey, path, paths, mode) => {
513
+ const optional = atKey.every(isOptional);
514
+ const creates = sharedReplacement(atKey);
515
+ const shared = buildShared(unionsMemberFields(atKey), path, paths, mode);
516
+ const takes = [];
517
+ if (shared.keys.length > 0 && (mode === "unchecked" || !atKey.some(isOptional))) takes.push(shared.patch);
518
+ if (creates !== void 0) takes.push(ReplaceSchema(creates));
519
+ if (optional) takes.push(DeleteField);
520
+ const whole = creates === void 0 ? void 0 : SharedValueSchema(creates);
521
+ const written = optional ? whole === void 0 ? DeleteField : DeleteFieldOr(whole) : whole;
522
+ if (written !== void 0) addPath(paths, path, written);
523
+ return takes.length === 0 ? void 0 : {
524
+ schema: takes.length === 1 ? takes[0] : Schema.Union(takes),
525
+ shape: shared.shape
526
+ };
527
+ };
528
+ /**
529
+ * Whether members holding `memberFields` take any key under `unchecked`, as
530
+ * {@link buildShared} would build it, but without building a schema.
531
+ */
532
+ const hasSharedField = (memberFields) => keysOf(memberFields).some((key) => {
533
+ const atKey = memberFields.filter((f) => Object.hasOwn(f, key)).map((f) => f[key]);
534
+ if (atKey.every(isStructField)) return atKey.every((field) => !isOptional(field)) ? hasSharedField(atKey.map(structFields)) : atKey.every(isOptional);
535
+ if (atKey.every(isUnionField)) return atKey.every(isOptional) || sharedReplacement(atKey) !== void 0 || hasSharedField(unionsMemberFields(atKey));
536
+ if (atKey.every((field) => !isNodeField(field))) {
537
+ const updates = atKey.map(updateSchema);
538
+ return updates.every((u) => u !== void 0) && haveValueInCommon(updates);
539
+ }
540
+ return false;
541
+ });
542
+ /** Builds the patch of a Record at the keys only the data names. */
543
+ const buildRest = (rest, path, paths, mode) => {
544
+ const entryPath = [...path, rest.key];
545
+ const node = nodeOf(rest.value, true);
546
+ let entry;
547
+ let shape;
548
+ if (node !== void 0) {
549
+ const built = buildNode({
550
+ ...node,
551
+ optional: true
552
+ }, entryPath, paths, mode);
553
+ entry = built.schema;
554
+ shape = built.shape;
555
+ } else {
556
+ entry = DeleteFieldOr(withoutConstructorDefault(withoutOptionalKey(updateSchema(rest.value))));
557
+ addPath(paths, entryPath, entry);
558
+ }
559
+ return {
560
+ patch: Schema.Record(Schema.toType(rest.key), entry),
561
+ shape: {
562
+ fields: {},
563
+ rest: { value: shape }
564
+ }
565
+ };
566
+ };
567
+ /**
568
+ * Goes one level down where the shape says a struct or a Record sits, unless it
569
+ * is being removed or replaced. It runs on the patch as the caller wrote it,
570
+ * before anything is encoded, so the removal and the replacement are still
571
+ * this library's to recognise.
572
+ *
573
+ * A replacement is written whole, as the struct it holds. A key of a Record
574
+ * fails where no path could name it; the paths it builds are encoded by the
575
+ * schema of each, keys and all.
576
+ */
577
+ const flatten = (value, shape, prefix, out, options) => Effect.gen(function* () {
578
+ for (const key of Object.keys(value)) {
579
+ const inner = value[key];
580
+ let child;
581
+ if (shape.rest !== void 0) {
582
+ const reason = unaddressable(key);
583
+ if (reason !== void 0) return yield* Effect.fail(new SchemaIssue.InvalidValue({ message: `Invalid Firestore field name "${key}" in "${prefix.slice(0, -1)}": it ${reason}` }, key, options));
584
+ child = shape.rest.value;
585
+ } else if (Object.hasOwn(shape.fields, key)) child = shape.fields[key];
586
+ if (child === void 0 || isDeleteField(inner)) out[prefix + key] = inner;
587
+ else if (isReplace(inner)) out[prefix + key] = inner.value;
588
+ else yield* flatten(inner, child, `${prefix}${key}.`, out, options);
589
+ }
590
+ return out;
591
+ });
592
+ /**
593
+ * The update of a document, out of the patch it takes, where the patch goes
594
+ * one level down, and the paths it writes at.
595
+ */
596
+ const updateOf = (patch, shape, paths) => {
597
+ const known = Schema.Struct(paths.known);
598
+ const patterns = [...paths.patterns.values()];
599
+ return (patterns.length === 0 ? known : Schema.StructWithRest(known, patterns)).pipe(Schema.decodeTo(Schema.toType(patch), {
600
+ decode: SchemaGetter.forbidden(() => "An update is only ever written, never read back"),
601
+ encode: SchemaGetter.transformEffect((value, options) => flatten(value, shape, "", {}, options))
602
+ }));
603
+ };
604
+ const update = (fields, mode) => {
605
+ const paths = emptyPaths();
606
+ const { patch, shape } = build(fields, [], paths, mode);
607
+ return updateOf(patch, shape, paths);
608
+ };
609
+ /**
610
+ * The update of a document that is a union, patched at its Shared Fields - and,
611
+ * unchecked, at a key only some members hold - by the rule a union field is
612
+ * patched by. It is never written whole: a document changes member through
613
+ * `set`.
614
+ */
615
+ const unionUpdate = (members, mode) => {
616
+ const paths = emptyPaths();
617
+ const { patch, shape } = buildShared(members.map(fields), [], paths, mode);
618
+ return updateOf(patch, shape, paths);
619
+ };
620
+ /** @internal */
621
+ const Class = (identifier) => (fields, annotations) => {
622
+ const Base = Variants.Class(identifier)(fields, annotations);
623
+ class Model extends Base {}
624
+ Object.defineProperty(Model, "update", { value: update(fields, "checked").annotate({
625
+ id: `${identifier}.update`,
626
+ title: `${identifier}.update`
627
+ }) });
628
+ Object.defineProperty(Model, "updateUnchecked", { value: update(fields, "unchecked").annotate({
629
+ id: `${identifier}.updateUnchecked`,
630
+ title: `${identifier}.updateUnchecked`
631
+ }) });
632
+ return Model;
633
+ };
634
+ /** @internal */
635
+ const Immutable = (schema) => Field({
636
+ read: schema,
637
+ create: schema
638
+ });
639
+ /** @internal */
640
+ const Timestamp = (schema) => Field({
641
+ read: schema,
642
+ create: ServerTimestampOr(schema),
643
+ update: ServerTimestampOr(schema)
644
+ });
645
+ /** @internal */
646
+ const Number = (schema) => Field({
647
+ read: schema,
648
+ create: IncrementOr(schema),
649
+ update: IncrementOr(schema)
650
+ });
651
+ /** @internal */
652
+ const Array$1 = (item) => Field({
653
+ read: Schema.Array(item),
654
+ create: Schema.Array(item),
655
+ update: ArrayOr(item)
656
+ });
657
+ const optionalField = fieldEvolve({
658
+ read: Schema.optionalKey,
659
+ create: Schema.optionalKey,
660
+ update: (schema) => Schema.optionalKey(DeleteFieldOr(schema))
661
+ });
662
+ /**
663
+ * What a struct or a Record takes as a field of its own, which is what
664
+ * `extract` hands back: the patches an unchecked update takes.
665
+ */
666
+ const nodeUpdate = (node) => Schema.optionalKey(buildNode(node, [], emptyPaths(), "unchecked").schema);
667
+ /** @internal */
668
+ const Record = (key, value) => {
669
+ const { rest } = recordKeys(key);
670
+ const of = (variant) => {
671
+ const schema = variantSchema(value, variant);
672
+ return schema && Schema.Record(key, rest ? withoutOptionalKey(schema) : schema);
673
+ };
674
+ const read = of("read");
675
+ const create = of("create");
676
+ const node = recordNode(key, value, () => create, false);
677
+ return Object.assign(Field({
678
+ ...read && { read },
679
+ ...create && { create },
680
+ ...node.writable && { update: nodeUpdate(node) }
681
+ }), { [RecordTypeId]: {
682
+ key,
683
+ value
684
+ } });
685
+ };
686
+ /** @internal */
687
+ const Union = (members) => {
688
+ const read = Schema.Union(Variants.Union(members).members);
689
+ const node = unionNode(members, false);
690
+ const schemas = {
691
+ read,
692
+ create: node.create(),
693
+ ...node.writable && { update: nodeUpdate(node) }
694
+ };
695
+ return Object.defineProperties(read, {
696
+ [FieldTypeId]: { value: FieldTypeId },
697
+ schemas: { value: schemas },
698
+ read: { value: Schema.Union(members.map((member) => extract(member, "read"))) },
699
+ create: { value: schemas.create },
700
+ update: { value: unionUpdate(members, "checked") },
701
+ updateUnchecked: { value: unionUpdate(members, "unchecked") },
702
+ [UnionTypeId]: { value: members }
703
+ });
704
+ };
705
+ const optionalNode = (self, node) => Field({
706
+ read: Schema.optionalKey(variantSchema(self, "read")),
707
+ create: Schema.optionalKey(node.create()),
708
+ ...node.writable && { update: nodeUpdate(node) }
709
+ });
710
+ /** @internal */
711
+ const Optional = (self) => {
712
+ if (isOptional(self)) return self;
713
+ const node = nodeOf(self, true);
714
+ return Object.assign(node === void 0 ? optionalField(self) : optionalNode(self, node), {
715
+ "~optionality": "optional",
716
+ field: self
717
+ });
718
+ };
719
+ export { Array$1 as Array, Class, Field, FieldExcept, FieldOnly, Immutable, Number, Optional, Record, RecordTypeId, Struct$1 as Struct, Timestamp, Union, UnionTypeId, extract, fieldEvolve, fields };