1import * as GCP from "@/GCP";
2import * as Effect from "effect/Effect";
3import { serveProbes } from "../../bindingHost.ts";
4
5const nativeDatabase = (id: string) =>
6 GCP.Firestore.Database(id, {
7 location: "us-central1",
8 type: "FIRESTORE_NATIVE",
9 });
10
11/** Database the per-document PatchDocument / GetDocument / DeleteDocument bind. */
12export const DocsDatabase = nativeDatabase("DocsDatabase");
13/** Database only {@link GCP.Firestore.ReadDatabase} binds (seeded by the test). */
14export const ReadOnlyDatabase = nativeDatabase("ReadOnlyDatabase");
15/** Database only {@link GCP.Firestore.WriteDatabase} binds. */
16export const WriteOnlyDatabase = nativeDatabase("WriteOnlyDatabase");
17/** Database only {@link GCP.Firestore.ReadWriteDatabase} binds. */
18export const ReadWriteOnlyDatabase = nativeDatabase("ReadWriteOnlyDatabase");
19
20/** Documents the test seeds out of band. */
21export const SEEDED_GET = "users/seeded";
22export const SEEDED_DELETE = "users/doomed";
23export const PATCHED = "users/alice";
24
25/**
26 * Effect-native Cloud Run service exercising every Firestore binding as its
27 * own runtime service account. Deployed from {@link ../Bindings.test.ts}.
28 */
29export default class FirestoreBindingsHost extends GCP.Function<FirestoreBindingsHost>()(
30 "FirestoreBindingsHost",
31 { main: import.meta.url, invokerIamDisabled: true },
32 Effect.gen(function* () {
33 const patchDocument = yield* GCP.Firestore.PatchDocument(DocsDatabase);
34 const getDocument = yield* GCP.Firestore.GetDocument(DocsDatabase);
35 const deleteDocument = yield* GCP.Firestore.DeleteDocument(DocsDatabase);
36 const reader = yield* GCP.Firestore.ReadDatabase(ReadOnlyDatabase);
37 const writer = yield* GCP.Firestore.WriteDatabase(WriteOnlyDatabase);
38 const both = yield* GCP.Firestore.ReadWriteDatabase(ReadWriteOnlyDatabase);
39
40 return {
41 fetch: serveProbes({
42 patchDocument: patchDocument({
43 documentPath: PATCHED,
44 body: { fields: { name: { stringValue: "Alice" } } },
45 }),
46 getDocument: getDocument({ documentPath: SEEDED_GET }),
47 deleteDocument: deleteDocument({ documentPath: SEEDED_DELETE }),
48 read: Effect.gen(function* () {
49 const one = yield* reader.get("things/a");
50 const missing = yield* reader.get("things/never");
51 const page1 = yield* reader.list("things", { pageSize: 1 });
52 const page2 = yield* reader.list("things", {
53 pageSize: 1,
54 pageToken: page1.nextPageToken,
55 });
56 const big = yield* reader.query({
57 from: [{ collectionId: "things" }],
58 where: {
59 fieldFilter: {
60 field: { fieldPath: "n" },
61 op: "GREATER_THAN",
62 value: { integerValue: "1" },
63 },
64 },
65 });
66 return {
67 one: one?.fields,
68 missing: missing === undefined,
69 listed: [...page1.documents, ...page2.documents]
70 .map((doc) => doc.name.split("/").pop())
71 .sort(),
72 hasNextPage: page1.nextPageToken !== undefined,
73 big: big.map((doc) => doc.fields.n),
74 };
75 }),
76 write: Effect.gen(function* () {
77 yield* writer.delete("things/created");
78 const created = yield* writer.create("things/created", { n: 1 });
79 const conflict = yield* writer
80 .create("things/created", { n: 2 })
81 .pipe(
82 Effect.map(() => "created"),
83 Effect.catchTag("GCP.Firestore.DocumentAlreadyExists", (error) =>
84 Effect.succeed(error.path),
85 ),
86 );
87 const updated = yield* writer.update("things/created", {
88 "odd key": "x",
89 });
90 const updateMissing = yield* writer
91 .update("things/never", { a: 1 })
92 .pipe(
93 Effect.map(() => "updated"),
94 Effect.catchTag("NotFound", () => Effect.succeed("NotFound")),
95 );
96 yield* writer.set("things/set", { team: "red" });
97 yield* writer.set("things/gone", { team: "blue" });
98 yield* writer.delete("things/gone");
99 yield* writer.delete("things/never-existed");
100 return {
101 created: created.name.endsWith("/documents/things/created"),
102 conflict,
103 updated: updated.fields,
104 updateMissing,
105 };
106 }),
107 readWrite: Effect.gen(function* () {
108 yield* both.delete("scores/a");
109 const missing = yield* both.get("scores/a");
110 yield* both.create("scores/a", { team: "red", n: 1 });
111 yield* both.set("scores/b", { team: "blue", n: 2 });
112 yield* both.update("scores/b", { n: 3 });
113 const b = yield* both.get("scores/b");
114 const listed = yield* both.list("scores");
115 const red = yield* both.query({
116 from: [{ collectionId: "scores" }],
117 where: {
118 fieldFilter: {
119 field: { fieldPath: "team" },
120 op: "EQUAL",
121 value: { stringValue: "red" },
122 },
123 },
124 });
125 yield* both.delete("scores/a");
126 const afterDelete = yield* both.get("scores/a");
127 return {
128 missing: missing === undefined,
129 b: b?.fields,
130 listed: listed.documents
131 .map((doc) => doc.name.split("/").pop())
132 .sort(),
133 red: red.map((doc) => doc.name.split("/").pop()),
134 afterDelete: afterDelete === undefined,
135 };
136 }),
137 }),
138 };
139 }).pipe(
140 Effect.provide(GCP.Firestore.PatchDocumentHttp),
141 Effect.provide(GCP.Firestore.GetDocumentHttp),
142 Effect.provide(GCP.Firestore.DeleteDocumentHttp),
143 Effect.provide(GCP.Firestore.ReadDatabaseHttp),
144 Effect.provide(GCP.Firestore.WriteDatabaseHttp),
145 Effect.provide(GCP.Firestore.ReadWriteDatabaseHttp),
146 ),
147) {}