1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as firestore from "@distilled.cloud/gcp/firestore_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const runLifecycle = !process.env.FAST;
18
19const waitUntilGone = (name: string) =>
20 firestore.getProjectsDatabasesCollectionGroupsIndexes({ name }).pipe(
21 Effect.as("found" as const),
22 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
23 Effect.repeat({
24 schedule: Schedule.spaced("2 seconds"),
25 until: (status) => status === "gone",
26 times: 10,
27 }),
28 );
29
30test.provider(
31 "getProjectsDatabasesCollectionGroupsIndexes on a missing index fails with NotFound",
32 (stack) =>
33 Effect.gen(function* () {
34 const { project } = yield* GcpEnvironment.current;
35
36 yield* stack.destroy();
37
38 const error = yield* Effect.flip(
39 firestore.getProjectsDatabasesCollectionGroupsIndexes({
40 name: `projects/${project}/databases/alchemy-missing-xxxx/collectionGroups/users/indexes/alchemy-missing`,
41 }),
42 );
43 expect(error._tag).toBe("NotFound");
44
45 yield* stack.destroy();
46 }).pipe(logLevel),
47 { tags: ["provider:gcp", "provider:gcp:firestore", "live"], timeout: 90_000 },
48);
49
50test.provider.skipIf(!runLifecycle)(
51 "create, replace, and delete a firestore composite index",
52 (stack) =>
53 Effect.gen(function* () {
54 yield* stack.destroy();
55
56 const created = yield* stack.deploy(
57 Effect.gen(function* () {
58 const database = yield* GCP.Firestore.Database("App", {
59 location: "us-central1",
60 type: "FIRESTORE_NATIVE",
61 });
62 const index = yield* GCP.Firestore.DatabasesCollectionGroupsIndex(
63 "UsersByName",
64 {
65 database: database.name,
66 collectionGroup: "users",
67 queryScope: "COLLECTION",
68 fields: [
69 { fieldPath: "name", order: "ASCENDING" },
70 { fieldPath: "created", order: "DESCENDING" },
71 ],
72 },
73 );
74 return { database, index };
75 }),
76 );
77
78 expect(created.index.name).toContain("/indexes/");
79 expect(created.index.databaseId).toEqual(created.database.databaseId);
80 expect(created.index.collectionGroupId).toEqual("users");
81 expect(created.index.queryScope).toEqual("COLLECTION");
82
83 const fetched =
84 yield* firestore.getProjectsDatabasesCollectionGroupsIndexes({
85 name: created.index.name,
86 });
87 expect(fetched.name).toEqual(created.index.name);
88 expect(fetched.queryScope).toEqual("COLLECTION");
89 expect(
90 (fetched.fields ?? []).some((field) => field.fieldPath === "name"),
91 ).toEqual(true);
92
93 const updated = yield* stack.deploy(
94 Effect.gen(function* () {
95 const database = yield* GCP.Firestore.Database("App", {
96 databaseId: created.database.databaseId,
97 location: "us-central1",
98 type: "FIRESTORE_NATIVE",
99 });
100 const index = yield* GCP.Firestore.DatabasesCollectionGroupsIndex(
101 "UsersByName",
102 {
103 database: database.name,
104 collectionGroup: "users",
105 queryScope: "COLLECTION",
106 fields: [
107 { fieldPath: "email", order: "ASCENDING" },
108 { fieldPath: "created", order: "DESCENDING" },
109 ],
110 },
111 );
112 return { database, index };
113 }),
114 );
115
116 expect(updated.index.collectionGroupId).toEqual("users");
117 expect(
118 updated.index.fields.some((field) => field.fieldPath === "email"),
119 ).toEqual(true);
120
121 const refetched =
122 yield* firestore.getProjectsDatabasesCollectionGroupsIndexes({
123 name: updated.index.name,
124 });
125 expect(
126 (refetched.fields ?? []).some((field) => field.fieldPath === "email"),
127 ).toEqual(true);
128
129 yield* stack.destroy();
130
131 const gone = yield* waitUntilGone(updated.index.name);
132 expect(gone).toEqual("gone");
133 }).pipe(logLevel),
134 {
135 tags: ["provider:gcp", "provider:gcp:firestore", "live"],
136 timeout: 180_000,
137 },
138);