@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,258 @@
1
+ /**
2
+ * Firebase Authentication on the Admin SDK, as an Effect service.
3
+ *
4
+ * {@link Auth} wraps the calls server code uses most, such as verifying ID
5
+ * tokens and managing users. Every call fails with an {@link AuthError}, and
6
+ * {@link Auth.use} reaches the rest of the Admin SDK.
7
+ *
8
+ * @example Verifying an ID token
9
+ * ```ts
10
+ * import { Effect, Layer } from "effect";
11
+ * import { App } from "@visiolab/effect-firebase-admin/app";
12
+ * import { Auth } from "@visiolab/effect-firebase-admin/auth";
13
+ *
14
+ * const uidOf = (idToken: string) =>
15
+ * Effect.gen(function* () {
16
+ * const auth = yield* Auth.Auth;
17
+ * const token = yield* auth.verifyIdToken(idToken, true);
18
+ * return token.uid;
19
+ * });
20
+ *
21
+ * const AuthLive = Auth.layer.pipe(Layer.provide(App.layer()));
22
+ *
23
+ * uidOf("<id token>").pipe(Effect.provide(AuthLive));
24
+ * ```
25
+ *
26
+ * @stability unstable
27
+ * @since 0.1.0
28
+ */
29
+ import { Context, Effect, Layer, Schema } from "effect";
30
+ import { FirebaseError } from "firebase-admin/app";
31
+ import * as Admin from "firebase-admin/auth";
32
+ import * as App from "../app/App.ts";
33
+
34
+ /**
35
+ * Fails a call on {@link Auth} when the Admin SDK rejects it, with what it
36
+ * threw as the `cause`.
37
+ *
38
+ * Match on `code` to tell failures apart, such as `auth/user-not-found` or
39
+ * `auth/id-token-revoked`.
40
+ *
41
+ * @stability unstable
42
+ * @category errors
43
+ * @since 0.1.0
44
+ */
45
+ export class AuthError extends Schema.TaggedError<AuthError, { readonly brand: unique symbol }>()(
46
+ "AuthError",
47
+ {
48
+ message: Schema.String,
49
+ /** The `service/string-code` the Admin SDK reported, such as `auth/user-not-found`. */
50
+ code: Schema.optional(Schema.String),
51
+ cause: Schema.optional(Schema.Defect()),
52
+ },
53
+ ) {}
54
+
55
+ const authError = (message: string) => (cause: unknown) =>
56
+ new AuthError({
57
+ message,
58
+ code: cause instanceof FirebaseError ? cause.code : undefined,
59
+ cause,
60
+ });
61
+
62
+ /**
63
+ * Firebase Authentication for a single app.
64
+ *
65
+ * Each call is the Admin SDK's method of the same name, with the same
66
+ * arguments, failing with an {@link AuthError} and traced as a span named
67
+ * `Auth.<method>`. Use {@link Auth.use} for anything else the Admin SDK offers.
68
+ *
69
+ * The Admin SDK cannot abort a request in flight, so interrupting a call does
70
+ * not stop it: the request still runs to completion.
71
+ *
72
+ * @stability unstable
73
+ * @category models
74
+ * @since 0.1.0
75
+ */
76
+ export interface Auth {
77
+ /**
78
+ * Runs `f` against the Admin SDK's own `Auth`, failing with an
79
+ * {@link AuthError} when it throws or rejects.
80
+ *
81
+ * **When to use**
82
+ *
83
+ * Use for the Admin SDK calls this service does not wrap, such as
84
+ * `setCustomUserClaims` or `listUsers`. Prefer the wrapped methods where one
85
+ * exists: they name the failure in the error's message.
86
+ *
87
+ * @example Setting custom claims
88
+ * ```ts
89
+ * import { Effect } from "effect";
90
+ * import { Auth } from "@visiolab/effect-firebase-admin/auth";
91
+ *
92
+ * const makeAdmin = (uid: string) =>
93
+ * Effect.gen(function* () {
94
+ * const auth = yield* Auth.Auth;
95
+ * yield* auth.use((sdk) => sdk.setCustomUserClaims(uid, { admin: true }));
96
+ * });
97
+ * ```
98
+ */
99
+ readonly use: <A>(f: (auth: Admin.Auth) => PromiseLike<A> | A) => Effect.Effect<A, AuthError>;
100
+ /**
101
+ * Verifies a Firebase ID token and decodes it.
102
+ *
103
+ * With `checkRevoked`, it also fails for a token whose session was revoked or
104
+ * whose user is disabled, at the cost of a request to the Auth backend.
105
+ *
106
+ * @see {@link Auth.createCustomToken} for minting a token a client signs in with
107
+ */
108
+ readonly verifyIdToken: (
109
+ idToken: string,
110
+ checkRevoked?: boolean,
111
+ ) => Effect.Effect<Admin.DecodedIdToken, AuthError>;
112
+ /**
113
+ * Mints a custom token for `uid`, which a client signs in with through
114
+ * `signInWithCustomToken`.
115
+ *
116
+ * `developerClaims` end up in the ID tokens of that session.
117
+ *
118
+ * @see {@link Auth.verifyIdToken} for checking the ID token a client sends back
119
+ */
120
+ readonly createCustomToken: (
121
+ uid: string,
122
+ developerClaims?: object,
123
+ ) => Effect.Effect<string, AuthError>;
124
+ /**
125
+ * Reads the user with `uid`.
126
+ *
127
+ * Fails with `auth/user-not-found` when there is none.
128
+ *
129
+ * @see {@link Auth.getUserByEmail} for looking a user up by email
130
+ */
131
+ readonly getUser: (uid: string) => Effect.Effect<Admin.UserRecord, AuthError>;
132
+ /**
133
+ * Reads the user with `email`.
134
+ *
135
+ * Fails with `auth/user-not-found` when there is none.
136
+ *
137
+ * @see {@link Auth.getUser} for looking a user up by uid
138
+ */
139
+ readonly getUserByEmail: (email: string) => Effect.Effect<Admin.UserRecord, AuthError>;
140
+ /**
141
+ * Creates a user, under the `uid` in `properties` or one the Auth backend
142
+ * picks.
143
+ *
144
+ * @see {@link Auth.updateUser} for changing a user that exists
145
+ */
146
+ readonly createUser: (
147
+ properties: Admin.CreateRequest,
148
+ ) => Effect.Effect<Admin.UserRecord, AuthError>;
149
+ /**
150
+ * Changes the properties of the user with `uid`.
151
+ *
152
+ * @see {@link Auth.createUser} for creating one
153
+ */
154
+ readonly updateUser: (
155
+ uid: string,
156
+ properties: Admin.UpdateRequest,
157
+ ) => Effect.Effect<Admin.UserRecord, AuthError>;
158
+ /**
159
+ * Deletes the user with `uid`.
160
+ */
161
+ readonly deleteUser: (uid: string) => Effect.Effect<void, AuthError>;
162
+ }
163
+
164
+ /**
165
+ * Service key for Firebase Authentication.
166
+ *
167
+ * @stability unstable
168
+ * @category services
169
+ * @since 0.1.0
170
+ */
171
+ export const Auth = Context.Service<Auth>("@visiolab/effect-firebase-admin/auth/Auth");
172
+
173
+ /**
174
+ * Builds {@link Auth} on the {@link App.App} in context.
175
+ *
176
+ * **When to use**
177
+ *
178
+ * Use to build the service inside an effect of your own. Most programs provide
179
+ * it with {@link layer} instead.
180
+ *
181
+ * **Details**
182
+ *
183
+ * Building the service cannot fail. `getAuth` only throws for an app that was
184
+ * already deleted or is not an app at all. Both are wiring mistakes, so they
185
+ * are defects, and {@link AuthError} only ever means that a call failed.
186
+ *
187
+ * @see {@link layer} for the same as a layer
188
+ *
189
+ * @stability unstable
190
+ * @category constructors
191
+ * @since 0.1.0
192
+ */
193
+ export const make = Effect.gen(function* () {
194
+ const app = yield* App.App;
195
+
196
+ // `getAuth` memoizes the service on the app itself, so there is nothing to
197
+ // release here.
198
+ const auth = Admin.getAuth(app);
199
+
200
+ const call = <A>(
201
+ span: string,
202
+ message: string,
203
+ f: (auth: Admin.Auth) => PromiseLike<A> | A,
204
+ ): Effect.Effect<A, AuthError> =>
205
+ Effect.tryPromise({
206
+ // `Promise.resolve` covers the calls that hand back a plain value, and
207
+ // keeps a synchronous throw on the `catch` path rather than the die one.
208
+ try: () => Promise.resolve(f(auth)),
209
+ catch: authError(message),
210
+ }).pipe(Effect.withSpan(`Auth.${span}`));
211
+
212
+ return Auth.of({
213
+ use: (f) => call("use", "A Firebase Authentication call failed", f),
214
+ verifyIdToken: (idToken, checkRevoked) =>
215
+ call("verifyIdToken", "Failed to verify the ID token", (auth) =>
216
+ auth.verifyIdToken(idToken, checkRevoked),
217
+ ),
218
+ createCustomToken: (uid, developerClaims) =>
219
+ call("createCustomToken", `Failed to create a custom token for "${uid}"`, (auth) =>
220
+ auth.createCustomToken(uid, developerClaims),
221
+ ),
222
+ getUser: (uid) =>
223
+ call("getUser", `Failed to get the user "${uid}"`, (auth) => auth.getUser(uid)),
224
+ getUserByEmail: (email) =>
225
+ call("getUserByEmail", `Failed to get the user with the email "${email}"`, (auth) =>
226
+ auth.getUserByEmail(email),
227
+ ),
228
+ createUser: (properties) =>
229
+ call("createUser", "Failed to create the user", (auth) => auth.createUser(properties)),
230
+ updateUser: (uid, properties) =>
231
+ call("updateUser", `Failed to update the user "${uid}"`, (auth) =>
232
+ auth.updateUser(uid, properties),
233
+ ),
234
+ deleteUser: (uid) =>
235
+ call("deleteUser", `Failed to delete the user "${uid}"`, (auth) => auth.deleteUser(uid)),
236
+ });
237
+ });
238
+
239
+ /**
240
+ * A layer that provides {@link Auth} for the {@link App.App} in context.
241
+ *
242
+ * @example Providing Auth
243
+ * ```ts
244
+ * import { Layer } from "effect";
245
+ * import { App } from "@visiolab/effect-firebase-admin/app";
246
+ * import { Auth } from "@visiolab/effect-firebase-admin/auth";
247
+ *
248
+ * const AuthLive = Auth.layer.pipe(Layer.provide(App.layer()));
249
+ * ```
250
+ *
251
+ * @see {@link make} for why building the service cannot fail
252
+ * @see {@link App.layer} for the app it needs
253
+ *
254
+ * @stability unstable
255
+ * @category layers
256
+ * @since 0.1.0
257
+ */
258
+ export const layer: Layer.Layer<Auth, never, App.App> = Layer.effect(Auth, make);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Firebase Authentication on the Admin SDK, as an Effect service.
3
+ *
4
+ * @stability unstable
5
+ * @since 0.1.0
6
+ */
7
+ export * as Auth from "./Auth.ts";
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Firestore on the Firebase Admin SDK, for servers.
3
+ *
4
+ * {@link layer} provides one service under two keys: the portable
5
+ * {@link Firestore}, which code that runs on either SDK asks for, and
6
+ * {@link AdminFirestore}, which adds what only the Admin SDK can do. Both reach
7
+ * the same database.
8
+ *
9
+ * @example Reading and writing a document
10
+ * ```ts
11
+ * import { Effect, Layer, Schema } from "effect";
12
+ * import { Collection, Firestore, FirestoreModel } from "@visiolab/effect-firebase/firestore";
13
+ * import { App } from "@visiolab/effect-firebase-admin/app";
14
+ * import { AdminFirestore } from "@visiolab/effect-firebase-admin/firestore";
15
+ *
16
+ * class Post extends FirestoreModel.Class<Post>("Post")({
17
+ * title: Schema.String,
18
+ * }) {}
19
+ *
20
+ * const Posts = Collection.make("posts", Post);
21
+ * const hello = Posts.doc(Posts.id.make("hello"));
22
+ *
23
+ * const program = Effect.gen(function* () {
24
+ * const firestore = yield* Firestore.Firestore;
25
+ * yield* firestore.setDoc(hello, { title: "Hello" });
26
+ * return yield* firestore.getDoc(hello);
27
+ * });
28
+ *
29
+ * const FirestoreLive = AdminFirestore.layer().pipe(Layer.provide(App.layer()));
30
+ *
31
+ * program.pipe(Effect.provide(FirestoreLive));
32
+ * ```
33
+ *
34
+ * @see {@link ClientFirestore} for the same service on the client SDK, for browsers
35
+ *
36
+ * @stability unstable
37
+ * @since 0.2.0
38
+ */
39
+ import { Context, Effect, Layer } from "effect";
40
+ import { Firestore, type With } from "@visiolab/effect-firebase/firestore/Firestore";
41
+ import type { Transaction, TransactionBase } from "@visiolab/effect-firebase/firestore/Firestore";
42
+ import type * as Admin from "firebase-admin/firestore";
43
+ import type * as App from "../app/App.ts";
44
+ import * as internalFirestore from "./internal/firestore.ts";
45
+
46
+ const TypeId = internalFirestore.TypeId;
47
+
48
+ const TransactionTypeId = internalFirestore.TransactionTypeId;
49
+
50
+ /**
51
+ * The portable {@link Firestore}, plus what only the Admin SDK can do.
52
+ *
53
+ * **When to use**
54
+ *
55
+ * Ask for it where code needs the Admin SDK: {@link AdminTransaction} reads of
56
+ * many documents, or the SDK itself through {@link AdminFirestore.instance}.
57
+ * Everywhere else ask for {@link Firestore}, so the code also runs on the
58
+ * client SDK.
59
+ *
60
+ * **Details**
61
+ *
62
+ * The service its key holds is also a {@link Firestore}, so it can be handed to
63
+ * portable code as it is. The type itself only extends {@link Firestore.With},
64
+ * so providing a {@link Firestore} alone, such as an in-memory fake, does not
65
+ * satisfy code that asks for it. A {@link Firestore.With.runTransaction} body
66
+ * finds its transaction both
67
+ * as {@link AdminTransaction} and as the portable
68
+ * {@link Firestore.Transaction}. They are the same transaction, so portable
69
+ * helpers and Admin-only ones can run in the same body.
70
+ *
71
+ * The Admin SDK cannot abort a request in flight, so interrupting a call does
72
+ * not stop it: the request still runs to completion.
73
+ *
74
+ * @see {@link Firestore.With} for each read and write it offers
75
+ *
76
+ * @stability unstable
77
+ * @category models
78
+ * @since 0.2.0
79
+ */
80
+ export interface AdminFirestore extends With<never, never, {}, AdminTransaction> {
81
+ readonly [TypeId]: typeof TypeId;
82
+
83
+ /**
84
+ * The Admin SDK's own `Firestore`, for the database the service was opened
85
+ * on.
86
+ *
87
+ * **When to use**
88
+ *
89
+ * Use for what this service does not wrap, such as queries, batched writes
90
+ * or `recursiveDelete`. Prefer the service's own methods where one exists.
91
+ *
92
+ * **Details**
93
+ *
94
+ * Calls through it skip the collection's model, take and return the SDK's
95
+ * own values, and throw the SDK's own errors. To write a model through it,
96
+ * encode with {@link FirestoreValue.toCodecFirestore} and map with
97
+ * {@link AdminFirestoreValue.toAdmin}.
98
+ *
99
+ * @see {@link AdminFirestoreValue} for mapping between the SDK's values and Firestore Values
100
+ */
101
+ readonly instance: Admin.Firestore;
102
+ }
103
+
104
+ /**
105
+ * A {@link Firestore.Transaction} that can also read every document in a
106
+ * collection or a Collection Group, which only the Admin SDK's transactions
107
+ * can.
108
+ *
109
+ * **When to use**
110
+ *
111
+ * Ask for it in a transaction body that reads many documents. A body that only
112
+ * reads single documents should ask for the portable
113
+ * {@link Firestore.Transaction}, so it also runs on the client SDK.
114
+ *
115
+ * **Details**
116
+ *
117
+ * A body run through {@link AdminFirestore} finds it next to
118
+ * {@link Firestore.Transaction}; they are the same transaction. The type
119
+ * extends {@link Firestore.TransactionBase}, not {@link Firestore.Transaction},
120
+ * and its key holds an `AdminTransaction & Transaction`. A body that asks for
121
+ * it and runs
122
+ * through the portable {@link Firestore} still needs it provided, so the
123
+ * compiler catches Admin-only code handed to portable code.
124
+ *
125
+ * The same rules hold as for {@link Firestore.Transaction}: a read of many is a
126
+ * read, so it comes before every write.
127
+ *
128
+ * @example Reading a collection in a transaction
129
+ * ```ts
130
+ * import { Effect, Schema } from "effect";
131
+ * import { Collection, FirestoreModel } from "@visiolab/effect-firebase/firestore";
132
+ * import { AdminFirestore } from "@visiolab/effect-firebase-admin/firestore";
133
+ *
134
+ * class Seat extends FirestoreModel.Class<Seat>("Seat")({
135
+ * taken: Schema.Boolean,
136
+ * }) {}
137
+ *
138
+ * const Seats = Collection.make("seats", Seat);
139
+ *
140
+ * const takeAnySeat = Effect.gen(function* () {
141
+ * const firestore = yield* AdminFirestore.AdminFirestore;
142
+ * return yield* firestore.runTransaction(
143
+ * Effect.gen(function* () {
144
+ * const tx = yield* AdminFirestore.AdminTransaction;
145
+ * const { docs } = yield* tx.getDocs(Seats.collection());
146
+ * for (const seat of docs) {
147
+ * if (!(yield* seat.data).taken) {
148
+ * yield* tx.updateDoc(seat.ref, { taken: true });
149
+ * return seat.id;
150
+ * }
151
+ * }
152
+ * return undefined;
153
+ * }),
154
+ * );
155
+ * });
156
+ * ```
157
+ *
158
+ * @see {@link Firestore.Transaction} for the portable transaction, which reads one document at a time
159
+ *
160
+ * @stability unstable
161
+ * @category models
162
+ * @since 0.2.0
163
+ */
164
+ export interface AdminTransaction extends TransactionBase {
165
+ readonly [TransactionTypeId]: typeof TransactionTypeId;
166
+
167
+ /**
168
+ * Reads every document in a collection or a Collection Group as part of the
169
+ * transaction, as {@link Firestore.With.getDocs} does outside one.
170
+ *
171
+ * Every document it returns is locked for the rest of the transaction, so
172
+ * another write to one of them makes the transaction run again. A group
173
+ * read that fails with an {@link Firestore.UndefinedCollectionError} fails
174
+ * the body, which rolls the transaction back without running it again.
175
+ */
176
+ readonly getDocs: Firestore["getDocs"];
177
+ }
178
+
179
+ /**
180
+ * Service key for {@link AdminTransaction}. The transaction it holds is a
181
+ * {@link Firestore.Transaction} too.
182
+ *
183
+ * @stability unstable
184
+ * @category services
185
+ * @since 0.2.0
186
+ */
187
+ export const AdminTransaction = Context.Service<AdminTransaction, AdminTransaction & Transaction>(
188
+ "@visiolab/effect-firebase-admin/firestore/AdminFirestore/AdminTransaction",
189
+ );
190
+
191
+ /**
192
+ * Service key for {@link AdminFirestore}.
193
+ *
194
+ * @see {@link Firestore} for the portable key, which {@link layer} provides the same service under
195
+ *
196
+ * @stability unstable
197
+ * @category services
198
+ * @since 0.2.0
199
+ */
200
+ export const AdminFirestore = Context.Service<AdminFirestore, AdminFirestore & Firestore>(
201
+ "@visiolab/effect-firebase-admin/firestore/AdminFirestore",
202
+ );
203
+
204
+ /**
205
+ * How {@link layer} opens a database. Both are fixed once the database is open.
206
+ *
207
+ * There is no emulator option. The Admin SDK reads the emulator from the
208
+ * `FIRESTORE_EMULATOR_HOST` environment variable, which must be set before the
209
+ * database is opened and applies to every app in the process.
210
+ *
211
+ * @see {@link ClientFirestore.Options} for the client SDK's, which take the emulator per database
212
+ *
213
+ * @stability unstable
214
+ * @category models
215
+ * @since 0.2.0
216
+ */
217
+ export interface Options {
218
+ /**
219
+ * The database to open, where the project has more than its default one.
220
+ * Leave it out for the default database.
221
+ */
222
+ readonly databaseId?: string | undefined;
223
+ /**
224
+ * Talk HTTP/1.1 instead of gRPC until something needs gRPC.
225
+ *
226
+ * Use it where a process handles a request or two and exits, such as a
227
+ * function: it saves loading the gRPC libraries and opening a channel.
228
+ * Listening is the one thing that needs gRPC; the first listen loads it and
229
+ * the SDK stays on it.
230
+ */
231
+ readonly preferRest?: boolean | undefined;
232
+ }
233
+
234
+ /**
235
+ * Builds {@link AdminFirestore} on the {@link App.App} in context.
236
+ *
237
+ * **When to use**
238
+ *
239
+ * Use to build the service inside an effect of your own. Most programs provide
240
+ * it with {@link layer} instead, which also provides it as {@link Firestore}.
241
+ *
242
+ * **Details**
243
+ *
244
+ * Building the service cannot fail. Opening a database only throws for an app
245
+ * that was already deleted, something that is not an app, or a database already
246
+ * opened with other settings. All three are wiring mistakes, so they are
247
+ * defects, and a {@link Firestore.FirestoreError} only ever means that a call
248
+ * failed.
249
+ *
250
+ * @see {@link layer} for the same as a layer
251
+ *
252
+ * @stability unstable
253
+ * @category constructors
254
+ * @since 0.2.0
255
+ */
256
+ export const make: (
257
+ options?: Options,
258
+ ) => Effect.Effect<AdminFirestore & Firestore, never, App.App> = Effect.fn("AdminFirestore.make")(
259
+ function* (options?: Options) {
260
+ const firestore = yield* internalFirestore.openDatabase(options);
261
+ return internalFirestore.fromDatabase(firestore, AdminTransaction);
262
+ },
263
+ );
264
+
265
+ /** The one service, under both its own tag and the portable one. */
266
+ const both = (service: AdminFirestore & Firestore) =>
267
+ Context.make(AdminFirestore, service).pipe(Context.add(Firestore, service));
268
+
269
+ /**
270
+ * A layer that provides {@link AdminFirestore} for the {@link App.App} in
271
+ * context, and the same service as the portable {@link Firestore}.
272
+ *
273
+ * **When to use**
274
+ *
275
+ * Use on a server, or anywhere the Admin SDK runs. In a browser, use
276
+ * {@link ClientFirestore.layer} instead.
277
+ *
278
+ * **Details**
279
+ *
280
+ * With no options it opens the project's default database with the SDK's
281
+ * defaults.
282
+ *
283
+ * @example Providing Firestore against the emulator
284
+ * ```ts
285
+ * import { Layer } from "effect";
286
+ * import { App } from "@visiolab/effect-firebase-admin/app";
287
+ * import { AdminFirestore } from "@visiolab/effect-firebase-admin/firestore";
288
+ *
289
+ * // The Admin SDK reads FIRESTORE_EMULATOR_HOST, such as "127.0.0.1:8080".
290
+ * const FirestoreLive = AdminFirestore.layer({ preferRest: true }).pipe(
291
+ * Layer.provide(App.layer({ projectId: "demo-project" })),
292
+ * );
293
+ * ```
294
+ *
295
+ * @see {@link make} for why building the service cannot fail
296
+ * @see {@link ClientFirestore.layer} for the client SDK's layer, for browsers
297
+ *
298
+ * @stability unstable
299
+ * @category layers
300
+ * @since 0.2.0
301
+ */
302
+ export const layer = (options?: Options): Layer.Layer<AdminFirestore | Firestore, never, App.App> =>
303
+ Layer.effectContext(Effect.map(make(options), both));
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Maps between Firestore Values, the portable values a model encodes to, and
3
+ * the values the Admin SDK reads and writes.
4
+ *
5
+ * {@link AdminFirestore} applies it on every read and write. Use it directly to
6
+ * read or write a model through the Admin SDK itself, such as through
7
+ * {@link AdminFirestore.instance}.
8
+ *
9
+ * @example Writing a model through the Admin SDK
10
+ * ```ts
11
+ * import { Schema } from "effect";
12
+ * import { FirestoreModel, FirestoreValue } from "@visiolab/effect-firebase/firestore";
13
+ * import { AdminFirestoreValue } from "@visiolab/effect-firebase-admin/firestore";
14
+ * import { type DocumentData, getFirestore } from "firebase-admin/firestore";
15
+ *
16
+ * class Post extends FirestoreModel.Class<Post>("Post")({
17
+ * title: Schema.String,
18
+ * }) {}
19
+ *
20
+ * const firestore = getFirestore();
21
+ * const encoded = Schema.encodeSync(FirestoreValue.toCodecFirestore(Post.create))({
22
+ * title: "Hello",
23
+ * });
24
+ * await firestore
25
+ * .doc("posts/hello")
26
+ * .set(AdminFirestoreValue.toAdmin(encoded, firestore) as DocumentData);
27
+ * ```
28
+ *
29
+ * @see {@link ClientFirestoreValue} for the client SDK's mapping
30
+ *
31
+ * @stability unstable
32
+ * @since 0.2.0
33
+ */
34
+ import type { Result } from "effect";
35
+ import type { FirestoreError } from "@visiolab/effect-firebase/firestore/Firestore";
36
+ import type { FirestoreValue } from "@visiolab/effect-firebase/firestore/FirestoreValue";
37
+ import type { Firestore } from "firebase-admin/firestore";
38
+ import * as internal from "./internal/firestoreValue.ts";
39
+
40
+ /**
41
+ * Turns a Firestore Value into the value the Admin SDK writes into
42
+ * `firestore`.
43
+ *
44
+ * **When to use**
45
+ *
46
+ * Use to write what a model encodes to through the Admin SDK itself.
47
+ *
48
+ * **Details**
49
+ *
50
+ * The portable `Timestamp`, `GeoPoint`, bytes and sentinels become the Admin
51
+ * SDK's own. A `DocumentPath` becomes `firestore.doc(path)`, so it points into
52
+ * that database. Map keys are kept as they are, so the dotted field paths of an
53
+ * update stay field paths.
54
+ *
55
+ * It throws for anything that is not a Firestore Value, such as one of the
56
+ * Admin SDK's own values or a class instance cast past the type. What
57
+ * {@link FirestoreValue.toCodecFirestore} encodes is always a Firestore Value.
58
+ *
59
+ * @see {@link fromAdmin} for the other direction
60
+ *
61
+ * @stability unstable
62
+ * @category conversions
63
+ * @since 0.2.0
64
+ */
65
+ export const toAdmin: (value: FirestoreValue, firestore: Firestore) => unknown = internal.toAdmin;
66
+
67
+ /**
68
+ * Turns a value the Admin SDK read into a Firestore Value.
69
+ *
70
+ * **When to use**
71
+ *
72
+ * Use to decode what the Admin SDK itself read through a model, with
73
+ * {@link FirestoreValue.toCodecFirestore}.
74
+ *
75
+ * **Details**
76
+ *
77
+ * The Admin SDK's `Timestamp`, `GeoPoint`, bytes and sentinels become their
78
+ * portable forms. A `DocumentReference` becomes the `DocumentPath` it points
79
+ * at, since Firestore stores nothing else of it;
80
+ * {@link FirestoreSchema.DocumentReference} reads that back into the collection
81
+ * it names.
82
+ *
83
+ * Fails with a {@link Firestore.FirestoreError} naming the field for a value
84
+ * that is not a Firestore Value, such as a vector.
85
+ *
86
+ * @see {@link toAdmin} for the other direction
87
+ *
88
+ * @stability unstable
89
+ * @category conversions
90
+ * @since 0.2.0
91
+ */
92
+ export const fromAdmin: (value: unknown) => Result.Result<FirestoreValue, FirestoreError> =
93
+ internal.fromAdmin;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Firestore on the Firebase Admin SDK, for servers.
3
+ *
4
+ * @stability unstable
5
+ * @since 0.2.0
6
+ */
7
+ export * as AdminFirestore from "./AdminFirestore.ts";
8
+ /**
9
+ * Maps between Firestore Values and the values the Admin SDK reads and writes.
10
+ *
11
+ * @stability unstable
12
+ * @since 0.2.0
13
+ */
14
+ export * as AdminFirestoreValue from "./AdminFirestoreValue.ts";