@visiolab/effect-firebase-admin 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -2
  3. package/dist/_virtual/_rolldown/runtime.mjs +12 -0
  4. package/dist/app/App.d.mts +115 -0
  5. package/dist/app/App.mjs +145 -0
  6. package/dist/app/App.mjs.map +1 -0
  7. package/dist/app/index.d.mts +2 -0
  8. package/dist/app/index.mjs +1 -0
  9. package/dist/app/internal/app.mjs +20 -0
  10. package/dist/app/internal/app.mjs.map +1 -0
  11. package/dist/auth/Auth.d.mts +167 -0
  12. package/dist/auth/Auth.mjs +131 -0
  13. package/dist/auth/Auth.mjs.map +1 -0
  14. package/dist/auth/index.d.mts +2 -0
  15. package/dist/auth/index.mjs +1 -0
  16. package/dist/firestore/AdminFirestore.d.mts +241 -0
  17. package/dist/firestore/AdminFirestore.mjs +133 -0
  18. package/dist/firestore/AdminFirestore.mjs.map +1 -0
  19. package/dist/firestore/AdminFirestoreValue.d.mts +61 -0
  20. package/dist/firestore/AdminFirestoreValue.mjs +62 -0
  21. package/dist/firestore/AdminFirestoreValue.mjs.map +1 -0
  22. package/dist/firestore/index.d.mts +3 -0
  23. package/dist/firestore/index.mjs +3 -0
  24. package/dist/firestore/internal/firestore.mjs +135 -0
  25. package/dist/firestore/internal/firestore.mjs.map +1 -0
  26. package/dist/firestore/internal/firestoreValue.mjs +56 -0
  27. package/dist/firestore/internal/firestoreValue.mjs.map +1 -0
  28. package/package.json +61 -3
  29. package/src/app/App.ts +158 -0
  30. package/src/app/index.ts +7 -0
  31. package/src/app/internal/app.ts +35 -0
  32. package/src/auth/Auth.ts +258 -0
  33. package/src/auth/index.ts +7 -0
  34. package/src/firestore/AdminFirestore.ts +303 -0
  35. package/src/firestore/AdminFirestoreValue.ts +93 -0
  36. package/src/firestore/index.ts +14 -0
  37. package/src/firestore/internal/firestore.ts +238 -0
  38. package/src/firestore/internal/firestoreValue.ts +85 -0
@@ -0,0 +1,238 @@
1
+ import { Context, Effect, Result } from "effect";
2
+ import * as Firestore from "@visiolab/effect-firebase/firestore/Firestore";
3
+ import { FirebaseError } from "firebase-admin/app";
4
+ import * as Admin from "firebase-admin/firestore";
5
+ import * as App from "../../app/App.ts";
6
+ import type * as AdminFirestore from "../AdminFirestore.ts";
7
+ import * as firestoreValue from "./firestoreValue.ts";
8
+
9
+ /** @internal */
10
+ export const TypeId = "~@visiolab/effect-firebase-admin/firestore/AdminFirestore";
11
+
12
+ /** @internal */
13
+ export const TransactionTypeId =
14
+ "~@visiolab/effect-firebase-admin/firestore/AdminFirestore/AdminTransaction";
15
+
16
+ /**
17
+ * Opens the database the options name.
18
+ *
19
+ * Settings can only be handed over through `initializeFirestore`, which refuses
20
+ * a database already opened with different ones, so they are only passed along
21
+ * when there are some: asking for the default database twice keeps working, and
22
+ * asking for it twice with settings that disagree is the mistake it reports.
23
+ */
24
+ const open = (app: App.App, options: AdminFirestore.Options | undefined): Admin.Firestore => {
25
+ const databaseId = options?.databaseId;
26
+ if (options?.preferRest === undefined) {
27
+ return databaseId === undefined ? Admin.getFirestore(app) : Admin.getFirestore(app, databaseId);
28
+ }
29
+ const settings = { preferRest: options.preferRest };
30
+ return databaseId === undefined
31
+ ? Admin.initializeFirestore(app, settings)
32
+ : Admin.initializeFirestore(app, settings, databaseId);
33
+ };
34
+
35
+ const firestoreError = (message: string) => (cause: unknown) =>
36
+ new Firestore.FirestoreError({
37
+ message,
38
+ // Firestore answers over gRPC, whose statuses are numbered rather than
39
+ // named, so a code only comes back for the failures the Admin SDK raises
40
+ // itself.
41
+ code: cause instanceof FirebaseError ? cause.code : undefined,
42
+ cause,
43
+ });
44
+
45
+ /**
46
+ * Opens the database the options name on the {@link App.App} in context.
47
+ *
48
+ * The SDK memoizes a database on the app itself, so there is nothing to release
49
+ * here.
50
+ *
51
+ * @internal
52
+ */
53
+ export const openDatabase = Effect.fnUntraced(function* (options?: AdminFirestore.Options) {
54
+ const app = yield* App.App;
55
+ return open(app, options);
56
+ });
57
+
58
+ /**
59
+ * Calls the Admin SDK, failing with `message` for whatever it throws or
60
+ * rejects with. Writes into a transaction are queued rather than sent, so they
61
+ * answer straight away - or throw, where the transaction refuses them.
62
+ */
63
+ const call = <A>(message: string, f: () => PromiseLike<A> | A) =>
64
+ Effect.tryPromise({ try: () => Promise.resolve(f()), catch: firestoreError(message) });
65
+
66
+ /**
67
+ * A document's data as Firestore Values, or `undefined` for one that is not
68
+ * there. A document holding a value outside the Firestore Value union, such as
69
+ * a vector, fails to read.
70
+ */
71
+ const dataOf = (
72
+ path: string,
73
+ data: Admin.DocumentData | undefined,
74
+ ): Result.Result<Firestore.DocumentData | undefined, Firestore.FirestoreError> =>
75
+ firestoreValue.readDocument(path, data) as Result.Result<
76
+ Firestore.DocumentData | undefined,
77
+ Firestore.FirestoreError
78
+ >;
79
+
80
+ const documentReadOf = (
81
+ path: string,
82
+ snapshot: Admin.DocumentSnapshot,
83
+ ): Result.Result<Firestore.DocumentRead, Firestore.FirestoreError> =>
84
+ Result.map(dataOf(path, snapshot.data()), (data) => ({ data }));
85
+
86
+ const queryReadOf = (
87
+ query: Admin.QuerySnapshot,
88
+ ): Result.Result<Firestore.QueryRead, Firestore.FirestoreError> =>
89
+ Result.map(
90
+ Result.all(
91
+ query.docs.map((snapshot) =>
92
+ // A document read as one of many is always there.
93
+ Result.map(dataOf(snapshot.ref.path, snapshot.data()), (data) => ({
94
+ path: snapshot.ref.path,
95
+ data: data as Firestore.DocumentData,
96
+ })),
97
+ ),
98
+ ),
99
+ (docs) => ({ docs }),
100
+ );
101
+
102
+ /** The Admin SDK query `selector` names, and what it is called in a failure. */
103
+ const queryOf = (firestore: Admin.Firestore, selector: Firestore.CollectionSelector) =>
104
+ selector._tag === "CollectionGroup"
105
+ ? {
106
+ query: (): Admin.Query => firestore.collectionGroup(selector.id),
107
+ name: `the collection group "${selector.id}"`,
108
+ }
109
+ : {
110
+ query: (): Admin.Query => firestore.collection(selector.path),
111
+ name: `the collection "${selector.path}"`,
112
+ };
113
+
114
+ /** Reads many documents, through the database or a transaction. */
115
+ const getDocs =
116
+ (firestore: Admin.Firestore, get: (query: Admin.Query) => Promise<Admin.QuerySnapshot>) =>
117
+ (selector: Firestore.CollectionSelector) => {
118
+ const { query, name } = queryOf(firestore, selector);
119
+ return Effect.flatMap(
120
+ call(`Failed to read ${name}`, () => get(query())),
121
+ (snapshot) => Effect.fromResult(queryReadOf(snapshot)),
122
+ );
123
+ };
124
+
125
+ /**
126
+ * The reads and writes of one attempt at a transaction, which, unlike the
127
+ * client's, can read many documents.
128
+ */
129
+ const transactionOperations = (
130
+ firestore: Admin.Firestore,
131
+ tx: Admin.Transaction,
132
+ ): Required<Firestore.TransactionOperations> => ({
133
+ getDoc: (path) =>
134
+ Effect.flatMap(
135
+ call(`Failed to read the document "${path}"`, () => tx.get(firestore.doc(path))),
136
+ (snapshot) => Effect.fromResult(documentReadOf(path, snapshot)),
137
+ ),
138
+ getDocs: getDocs(firestore, (query) => tx.get(query)),
139
+ setDoc: (path, data) =>
140
+ call(`Failed to write the document "${path}"`, () => {
141
+ tx.set(firestore.doc(path), firestoreValue.toAdmin(data, firestore) as Admin.DocumentData);
142
+ }),
143
+ updateDoc: (path, data) =>
144
+ call(`Failed to update the document "${path}"`, () => {
145
+ tx.update(
146
+ firestore.doc(path),
147
+ firestoreValue.toAdmin(data, firestore) as Admin.UpdateData<Admin.DocumentData>,
148
+ );
149
+ }),
150
+ });
151
+
152
+ /**
153
+ * The service over a database already opened.
154
+ *
155
+ * The key a transaction body finds its
156
+ * {@link AdminFirestore.AdminTransaction} under is handed in by the public
157
+ * module that defines it, so that this one only imports it as a type and the
158
+ * two do not import each other.
159
+ *
160
+ * @internal
161
+ */
162
+ export const fromDatabase = (
163
+ firestore: Admin.Firestore,
164
+ AdminTransaction: typeof AdminFirestore.AdminTransaction,
165
+ ): AdminFirestore.AdminFirestore & Firestore.Firestore => {
166
+ const toAdmin = (data: Firestore.DocumentData) => firestoreValue.toAdmin(data, firestore);
167
+
168
+ const service = Firestore.make({
169
+ getDoc: (path) =>
170
+ Effect.flatMap(
171
+ call(`Failed to read the document "${path}"`, () => firestore.doc(path).get()),
172
+ (snapshot) => Effect.fromResult(documentReadOf(path, snapshot)),
173
+ ),
174
+
175
+ getDocs: getDocs(firestore, (query) => query.get()),
176
+
177
+ setDoc: (path, data) =>
178
+ Effect.asVoid(
179
+ call(`Failed to write the document "${path}"`, () =>
180
+ firestore.doc(path).set(toAdmin(data) as Admin.DocumentData),
181
+ ),
182
+ ),
183
+
184
+ updateDoc: (path, data) =>
185
+ Effect.asVoid(
186
+ call(`Failed to update the document "${path}"`, () =>
187
+ firestore.doc(path).update(toAdmin(data) as Admin.UpdateData<Admin.DocumentData>),
188
+ ),
189
+ ),
190
+
191
+ addDoc: (path, data) =>
192
+ Effect.map(
193
+ call(`Failed to add a document to "${path}"`, () =>
194
+ firestore.collection(path).add(toAdmin(data) as Admin.DocumentData),
195
+ ),
196
+ (added) => added.id,
197
+ ),
198
+
199
+ streamDoc: (path) =>
200
+ Firestore.streamFromListener({
201
+ subscribe: (onNext, onError) => firestore.doc(path).onSnapshot(onNext, onError),
202
+ onError: firestoreError(`Failed to listen to the document "${path}"`),
203
+ map: (snapshot: Admin.DocumentSnapshot) => documentReadOf(path, snapshot),
204
+ }),
205
+
206
+ streamDocs: (selector) => {
207
+ const { query, name } = queryOf(firestore, selector);
208
+ return Firestore.streamFromListener({
209
+ subscribe: (onNext, onError) => query().onSnapshot(onNext, onError),
210
+ onError: firestoreError(`Failed to listen to ${name}`),
211
+ map: queryReadOf,
212
+ });
213
+ },
214
+
215
+ runTransaction: Firestore.transactionFromCallback({
216
+ run: (callback, options) => firestore.runTransaction(callback, options),
217
+ operations: (tx: Admin.Transaction) => transactionOperations(firestore, tx),
218
+ onError: firestoreError("Failed to run a transaction"),
219
+ // The operations read many documents, so the transaction core builds
220
+ // has a `getDocs`. Branding it in place, rather than a copy, keeps one
221
+ // object under both keys.
222
+ context: (transaction) =>
223
+ Context.make(
224
+ AdminTransaction,
225
+ Object.assign(transaction, { [TransactionTypeId]: TransactionTypeId } as const),
226
+ ),
227
+ }),
228
+ });
229
+
230
+ // The Admin interface is checked against what `make` built here, except for
231
+ // the Admin Transaction its `context` offers: the compiler does not check
232
+ // transaction services on assignment, so the AdminTransaction tests do.
233
+ const admin: AdminFirestore.AdminFirestore & Firestore.Firestore = Object.assign(service, {
234
+ [TypeId]: TypeId as typeof TypeId,
235
+ instance: firestore,
236
+ });
237
+ return admin;
238
+ };
@@ -0,0 +1,85 @@
1
+ import { Predicate, type Result } from "effect";
2
+ import * as DocumentPath from "@visiolab/effect-firebase/firestore/DocumentPath";
3
+ import * as FieldValue from "@visiolab/effect-firebase/firestore/FieldValue";
4
+ import type { FirestoreError } from "@visiolab/effect-firebase/firestore/Firestore";
5
+ import * as FirestoreValue from "@visiolab/effect-firebase/firestore/FirestoreValue";
6
+ import * as GeoPoint from "@visiolab/effect-firebase/firestore/GeoPoint";
7
+ import * as Timestamp from "@visiolab/effect-firebase/firestore/Timestamp";
8
+ import * as Admin from "firebase-admin/firestore";
9
+
10
+ /** How the Admin SDK writes each kind of Firestore Value into `firestore`. */
11
+ const writer = (firestore: Admin.Firestore): FirestoreValue.Writer => ({
12
+ bytes: (bytes) => bytes,
13
+ timestamp: (ts) => new Admin.Timestamp(ts.seconds, ts.nanoseconds),
14
+ geoPoint: (point) => new Admin.GeoPoint(point.latitude, point.longitude),
15
+ documentPath: (ref) => firestore.doc(ref.path),
16
+ serverTimestamp: () => Admin.FieldValue.serverTimestamp(),
17
+ increment: (increment) => Admin.FieldValue.increment(increment.by),
18
+ deleteField: () => Admin.FieldValue.delete(),
19
+ arrayUnion: (union, walk) => Admin.FieldValue.arrayUnion(...union.elements.map(walk)),
20
+ arrayRemove: (remove, walk) => Admin.FieldValue.arrayRemove(...remove.elements.map(walk)),
21
+ });
22
+
23
+ /** @internal */
24
+ export const toAdmin = (
25
+ value: FirestoreValue.FirestoreValue,
26
+ firestore: Admin.Firestore,
27
+ ): unknown => FirestoreValue.toSdk(value, writer(firestore));
28
+
29
+ /** The sentinels' names, as the Admin SDK keeps them. */
30
+ interface AdminSentinel {
31
+ readonly methodName?: string;
32
+ readonly operand?: number;
33
+ readonly elements?: ReadonlyArray<unknown>;
34
+ }
35
+
36
+ /** How the Admin SDK's own values read back as Firestore Values. */
37
+ const reader: FirestoreValue.Reader = (u, { walk, fail }) => {
38
+ if (u instanceof Uint8Array) {
39
+ // The Admin SDK reads bytes as a Node `Buffer`.
40
+ return u.constructor === Uint8Array ? u : new Uint8Array(u.buffer, u.byteOffset, u.length);
41
+ }
42
+ if (u instanceof Admin.Timestamp) return Timestamp.makeUnsafe(u.seconds, u.nanoseconds);
43
+ if (u instanceof Admin.GeoPoint) return GeoPoint.makeUnsafe(u.latitude, u.longitude);
44
+ if (u instanceof Admin.DocumentReference) return DocumentPath.makeUnsafe(u.path);
45
+ if (u instanceof Admin.FieldValue) {
46
+ const sentinel = u as AdminSentinel;
47
+ switch (sentinel.methodName) {
48
+ case "FieldValue.serverTimestamp":
49
+ return FieldValue.serverTimestamp;
50
+ case "FieldValue.delete":
51
+ return FieldValue.deleteField;
52
+ case "FieldValue.increment":
53
+ if (Predicate.isNumber(sentinel.operand)) return FieldValue.increment(sentinel.operand);
54
+ break;
55
+ case "FieldValue.arrayUnion":
56
+ if (Array.isArray(sentinel.elements))
57
+ return FieldValue.arrayUnion(...sentinel.elements.map(walk));
58
+ break;
59
+ case "FieldValue.arrayRemove":
60
+ if (Array.isArray(sentinel.elements))
61
+ return FieldValue.arrayRemove(...sentinel.elements.map(walk));
62
+ break;
63
+ }
64
+ return fail(`the sentinel ${sentinel.methodName ?? u.constructor.name}`);
65
+ }
66
+ return undefined;
67
+ };
68
+
69
+ /** @internal */
70
+ export const fromAdmin = (
71
+ value: unknown,
72
+ ): Result.Result<FirestoreValue.FirestoreValue, FirestoreError> =>
73
+ FirestoreValue.fromSdk(value, reader);
74
+
75
+ /**
76
+ * What the Admin SDK read for the document at `path`, as Firestore Values, or
77
+ * `undefined` for a document that is not there.
78
+ *
79
+ * @internal
80
+ */
81
+ export const readDocument = (
82
+ path: string,
83
+ data: Admin.DocumentData | undefined,
84
+ ): Result.Result<FirestoreValue.FirestoreValue | undefined, FirestoreError> =>
85
+ FirestoreValue.readDocument(path, data, reader);