@visiolab/effect-firebase-admin 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 +73 -2
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/app/App.d.mts +115 -0
- package/dist/app/App.mjs +145 -0
- package/dist/app/App.mjs.map +1 -0
- package/dist/app/index.d.mts +2 -0
- package/dist/app/index.mjs +1 -0
- package/dist/app/internal/app.mjs +20 -0
- package/dist/app/internal/app.mjs.map +1 -0
- package/dist/auth/Auth.d.mts +167 -0
- package/dist/auth/Auth.mjs +131 -0
- package/dist/auth/Auth.mjs.map +1 -0
- package/dist/auth/index.d.mts +2 -0
- package/dist/auth/index.mjs +1 -0
- package/dist/firestore/AdminFirestore.d.mts +241 -0
- package/dist/firestore/AdminFirestore.mjs +133 -0
- package/dist/firestore/AdminFirestore.mjs.map +1 -0
- package/dist/firestore/AdminFirestoreValue.d.mts +61 -0
- package/dist/firestore/AdminFirestoreValue.mjs +62 -0
- package/dist/firestore/AdminFirestoreValue.mjs.map +1 -0
- package/dist/firestore/index.d.mts +3 -0
- package/dist/firestore/index.mjs +3 -0
- package/dist/firestore/internal/firestore.mjs +135 -0
- package/dist/firestore/internal/firestore.mjs.map +1 -0
- package/dist/firestore/internal/firestoreValue.mjs +56 -0
- package/dist/firestore/internal/firestoreValue.mjs.map +1 -0
- package/package.json +61 -3
- package/src/app/App.ts +158 -0
- package/src/app/index.ts +7 -0
- package/src/app/internal/app.ts +35 -0
- package/src/auth/Auth.ts +258 -0
- package/src/auth/index.ts +7 -0
- package/src/firestore/AdminFirestore.ts +303 -0
- package/src/firestore/AdminFirestoreValue.ts +93 -0
- package/src/firestore/index.ts +14 -0
- package/src/firestore/internal/firestore.ts +238 -0
- 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);
|