Skip to content

Commit 7aa8b8a

Browse files
committed
feat: Pass admin app through a service
1 parent d36922a commit 7aa8b8a

7 files changed

Lines changed: 196 additions & 135 deletions

File tree

packages/admin/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * as Admin from './lib/admin.js';
2+
export * as App from './lib/app.js';
23
export * as FunctionsRuntime from './lib/runtime.js';
34
export * as Logger from './lib/logger.js';
45
export * as Firestore from './lib/firestore/firestore-service.js';

packages/admin/src/lib/admin.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import type { App as FirebaseAdminApp } from 'firebase-admin/app';
12
import { type FirestoreService } from 'effect-firebase';
23
import { layer as firestoreLayer } from './firestore/firestore-service.js';
34
import { cloudConsole } from './logger.js';
45
import { Layer } from 'effect';
6+
import { App, layer as appLayer } from './app.js';
57

6-
export const layer: Layer.Layer<FirestoreService, never, never> = Layer.merge(
8+
export const layer: Layer.Layer<FirestoreService, never, App> = Layer.merge(
79
firestoreLayer,
810
cloudConsole
911
);
12+
13+
export const layerFromApp = (
14+
app: FirebaseAdminApp
15+
): Layer.Layer<FirestoreService, never, never> =>
16+
Layer.provide(layer, appLayer(app));

packages/admin/src/lib/app.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { App as FirebaseAdminApp } from 'firebase-admin/app';
2+
import { Context, Layer } from 'effect';
3+
4+
export interface AppService {
5+
readonly getApp: () => FirebaseAdminApp;
6+
}
7+
8+
export class App extends Context.Tag('@effect-firebase/admin/App')<
9+
App,
10+
AppService
11+
>() {}
12+
13+
export const layer = (app: FirebaseAdminApp): Layer.Layer<App> =>
14+
Layer.succeed(App, {
15+
getApp: () => app,
16+
});

packages/admin/src/lib/firestore/converter.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import {
22
DocumentData,
33
DocumentReference,
44
FieldValue,
5+
Firestore,
56
FirestoreDataConverter,
67
GeoPoint,
7-
getFirestore,
88
Timestamp,
99
} from 'firebase-admin/firestore';
1010
import { FirestoreSchema } from 'effect-firebase';
1111

12-
export const toFirestoreDocumentData = (data: DocumentData): DocumentData => {
12+
export const toFirestoreDocumentData = (
13+
db: Firestore,
14+
data: DocumentData
15+
): DocumentData => {
1316
if (
1417
data === null ||
1518
data instanceof Timestamp ||
@@ -27,17 +30,17 @@ export const toFirestoreDocumentData = (data: DocumentData): DocumentData => {
2730
return new GeoPoint(data.latitude, data.longitude);
2831
}
2932
if (data instanceof FirestoreSchema.Reference) {
30-
return getFirestore().doc(data.path);
33+
return db.doc(data.path);
3134
}
3235
if (data instanceof FirestoreSchema.ServerTimestamp) {
3336
return FieldValue.serverTimestamp();
3437
}
3538
if (Array.isArray(data)) {
36-
return data.map(toFirestoreDocumentData);
39+
return data.map((item) => toFirestoreDocumentData(db, item));
3740
}
3841
if (typeof data === 'object' && data !== null) {
3942
return Object.fromEntries(
40-
Object.entries(data).map(([k, v]) => [k, toFirestoreDocumentData(v)])
43+
Object.entries(data).map(([k, v]) => [k, toFirestoreDocumentData(db, v)])
4144
);
4245
}
4346
return data;
@@ -67,7 +70,9 @@ export const fromFirestoreDocumentData = (data: DocumentData): DocumentData => {
6770
return data;
6871
};
6972

70-
export const converter: FirestoreDataConverter<DocumentData, DocumentData> = {
71-
toFirestore: (modelObject) => toFirestoreDocumentData(modelObject),
73+
export const makeConverter = (
74+
db: Firestore
75+
): FirestoreDataConverter<DocumentData, DocumentData> => ({
76+
toFirestore: (modelObject) => toFirestoreDocumentData(db, modelObject),
7277
fromFirestore: (snapshot) => fromFirestoreDocumentData(snapshot.data()),
73-
};
78+
});

packages/admin/src/lib/firestore/firestore-service.ts

Lines changed: 105 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
import type { Snapshot } from 'effect-firebase';
88
import { UnknownException } from 'effect/Cause';
99
import { getFirestore } from 'firebase-admin/firestore';
10-
import { converter, fromFirestoreDocumentData } from './converter.js';
10+
import { App } from '../app.js';
11+
import { fromFirestoreDocumentData, makeConverter } from './converter.js';
1112
import { buildQuery } from './query-builder.js';
1213

1314
const packSnapshot = makeSnapshotPacker(fromFirestoreDocumentData);
@@ -20,105 +21,111 @@ const mapError = (error: unknown) =>
2021
/**
2122
* Live Firestore service using the admin SDK.
2223
*/
23-
export const layer = Layer.succeed(
24+
export const layer = Layer.effect(
2425
FirestoreService,
25-
FirestoreService.of({
26-
get: (path, options) =>
27-
Effect.tryPromise({
28-
try: () => getFirestore().doc(path).get(),
29-
catch: (error) => mapError(error),
30-
}).pipe(Effect.map((snapshot) => packSnapshot(snapshot, options))),
31-
add: (path, data) =>
32-
Effect.tryPromise({
33-
try: async () => {
34-
const ref = await getFirestore()
35-
.collection(path)
36-
.withConverter(converter)
37-
.add(data);
38-
return { id: ref.id, path: ref.path };
39-
},
40-
catch: (error) => mapError(error),
41-
}),
42-
set: (path, data, options) =>
43-
Effect.tryPromise({
44-
try: () =>
45-
getFirestore()
46-
.doc(path)
47-
.withConverter(converter)
48-
.set(data, options || {}),
49-
catch: (error) => mapError(error),
50-
}),
51-
update: (path, data) =>
52-
Effect.tryPromise({
53-
try: () => getFirestore().doc(path).update(converter.toFirestore(data)),
54-
catch: (error) => mapError(error),
55-
}),
56-
remove: (path) =>
57-
Effect.tryPromise({
58-
try: () => getFirestore().doc(path).withConverter(converter).delete(),
59-
catch: (error) => mapError(error),
60-
}),
61-
query: (collectionPath, constraints) =>
62-
Effect.tryPromise({
63-
try: async () => {
64-
const query = buildQuery(collectionPath, constraints);
65-
const snapshot = await query.get();
66-
return Arr.filterMap(
67-
snapshot.docs,
68-
(doc): Option.Option<Snapshot> => packSnapshot(doc)
69-
);
70-
},
71-
catch: (error) => mapError(error),
72-
}),
73-
streamDoc: (path, options) =>
74-
Stream.asyncScoped<Option.Option<Snapshot>, FirestoreError>((emit) =>
75-
Effect.acquireRelease(
76-
Effect.sync(() => {
77-
const docRef = getFirestore().doc(path);
78-
return docRef.onSnapshot(
79-
(snapshot) => {
80-
emit.single(packSnapshot(snapshot, options));
81-
},
82-
(error) => {
83-
const mappedError = mapError(error);
84-
if (mappedError._tag === 'FirestoreError') {
85-
emit.fail(mappedError);
86-
} else {
87-
// Convert UnknownException to FirestoreError
88-
emit.fail(FirestoreError.fromError(error as Error));
89-
}
90-
}
26+
Effect.gen(function* () {
27+
const app = yield* App;
28+
const db = getFirestore(app.getApp());
29+
const converter = makeConverter(db);
30+
31+
return FirestoreService.of({
32+
get: (path, options) =>
33+
Effect.tryPromise({
34+
try: () => db.doc(path).get(),
35+
catch: (error) => mapError(error),
36+
}).pipe(Effect.map((snapshot) => packSnapshot(snapshot, options))),
37+
add: (path, data) =>
38+
Effect.tryPromise({
39+
try: async () => {
40+
const ref = await db
41+
.collection(path)
42+
.withConverter(converter)
43+
.add(data);
44+
return { id: ref.id, path: ref.path };
45+
},
46+
catch: (error) => mapError(error),
47+
}),
48+
set: (path, data, options) =>
49+
Effect.tryPromise({
50+
try: () =>
51+
db
52+
.doc(path)
53+
.withConverter(converter)
54+
.set(data, options || {}),
55+
catch: (error) => mapError(error),
56+
}),
57+
update: (path, data) =>
58+
Effect.tryPromise({
59+
try: () => db.doc(path).update(converter.toFirestore(data)),
60+
catch: (error) => mapError(error),
61+
}),
62+
remove: (path) =>
63+
Effect.tryPromise({
64+
try: () => db.doc(path).withConverter(converter).delete(),
65+
catch: (error) => mapError(error),
66+
}),
67+
query: (collectionPath, constraints) =>
68+
Effect.tryPromise({
69+
try: async () => {
70+
const query = buildQuery(db, collectionPath, constraints);
71+
const snapshot = await query.get();
72+
return Arr.filterMap(
73+
snapshot.docs,
74+
(doc): Option.Option<Snapshot> => packSnapshot(doc)
9175
);
92-
}),
93-
(unsubscribe) => Effect.sync(() => unsubscribe())
94-
)
95-
),
96-
streamQuery: (collectionPath, constraints, options) =>
97-
Stream.asyncScoped<ReadonlyArray<Snapshot>, FirestoreError>((emit) =>
98-
Effect.acquireRelease(
99-
Effect.sync(() => {
100-
const query = buildQuery(collectionPath, constraints);
101-
return query.onSnapshot(
102-
(snapshot) => {
103-
const snapshots = Arr.filterMap(
104-
snapshot.docs,
105-
(doc): Option.Option<Snapshot> => packSnapshot(doc, options)
106-
);
107-
emit.single(snapshots);
108-
},
109-
(error) => {
110-
const mappedError = mapError(error);
111-
if (mappedError._tag === 'FirestoreError') {
112-
emit.fail(mappedError);
113-
} else {
114-
// Convert UnknownException to FirestoreError
115-
emit.fail(FirestoreError.fromError(error as Error));
76+
},
77+
catch: (error) => mapError(error),
78+
}),
79+
streamDoc: (path, options) =>
80+
Stream.asyncScoped<Option.Option<Snapshot>, FirestoreError>((emit) =>
81+
Effect.acquireRelease(
82+
Effect.sync(() => {
83+
const docRef = db.doc(path);
84+
return docRef.onSnapshot(
85+
(snapshot) => {
86+
emit.single(packSnapshot(snapshot, options));
87+
},
88+
(error) => {
89+
const mappedError = mapError(error);
90+
if (mappedError._tag === 'FirestoreError') {
91+
emit.fail(mappedError);
92+
} else {
93+
// Convert UnknownException to FirestoreError
94+
emit.fail(FirestoreError.fromError(error as Error));
95+
}
11696
}
117-
}
118-
);
119-
}),
120-
(unsubscribe) => Effect.sync(() => unsubscribe())
121-
)
122-
),
97+
);
98+
}),
99+
(unsubscribe) => Effect.sync(() => unsubscribe())
100+
)
101+
),
102+
streamQuery: (collectionPath, constraints, options) =>
103+
Stream.asyncScoped<ReadonlyArray<Snapshot>, FirestoreError>((emit) =>
104+
Effect.acquireRelease(
105+
Effect.sync(() => {
106+
const query = buildQuery(db, collectionPath, constraints);
107+
return query.onSnapshot(
108+
(snapshot) => {
109+
const snapshots = Arr.filterMap(
110+
snapshot.docs,
111+
(doc): Option.Option<Snapshot> => packSnapshot(doc, options)
112+
);
113+
emit.single(snapshots);
114+
},
115+
(error) => {
116+
const mappedError = mapError(error);
117+
if (mappedError._tag === 'FirestoreError') {
118+
emit.fail(mappedError);
119+
} else {
120+
// Convert UnknownException to FirestoreError
121+
emit.fail(FirestoreError.fromError(error as Error));
122+
}
123+
}
124+
);
125+
}),
126+
(unsubscribe) => Effect.sync(() => unsubscribe())
127+
)
128+
),
129+
});
123130
})
124131
);

0 commit comments

Comments
 (0)