Skip to content

Commit f1498cf

Browse files
committed
docs(changeset): Add Firestore database service
1 parent ff3c91b commit f1498cf

File tree

8 files changed

+258
-12
lines changed

8 files changed

+258
-12
lines changed

.changeset/legal-trees-double.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@storybooker/gcp": minor
3+
---
4+
5+
Add Firestore database service

packages/gcp/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Create service adapters for GCP services.
88
99
## Database
1010

11-
GCP provides 3 options which can be used as database for StoryBooker.
11+
GCP provides 2 options which can be used as database for StoryBooker.
1212

1313
### BigTable
1414

@@ -22,6 +22,16 @@ const database = new GcpBigtableDatabaseService({
2222
// use as database in StoryBooker options.
2323
```
2424

25+
```ts
26+
import { GcpFirestoreDatabaseService } from "@storybooker/gcp/firestore";
27+
28+
const database = new GcpFirestoreDatabaseService({
29+
// Auth options can be passed here.
30+
});
31+
32+
// use as database in StoryBooker options.
33+
```
34+
2535
## Storage
2636

2737
The Google Cloud Storage provides BlobStorage which can be used as storage for StoryBooker.

packages/gcp/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
"import": "./dist/big-table.js",
5252
"require": "./dist/big-table.cjs"
5353
},
54+
"./firestore": {
55+
"source": "./src/firestore.ts",
56+
"import": "./dist/firestore.js",
57+
"require": "./dist/firestore.cjs"
58+
},
5459
"./storage": {
5560
"source": "./src/storage.ts",
5661
"import": "./dist/storage.js",
@@ -69,6 +74,7 @@
6974
},
7075
"devDependencies": {
7176
"@google-cloud/bigtable": "^6.4.1",
77+
"@google-cloud/firestore": "^7.11.5",
7278
"@google-cloud/storage": "^7.17.1",
7379
"@storybooker/core": "workspace:^",
7480
"@types/node": "^22.0.0",
@@ -80,13 +86,17 @@
8086
},
8187
"peerDependencies": {
8288
"@google-cloud/bigtable": "^6.0.0",
89+
"@google-cloud/firestore": "^7.0.0",
8390
"@google-cloud/storage": "^7.0.0",
8491
"@storybooker/core": "workspace:^"
8592
},
8693
"peerDependenciesMeta": {
8794
"@google-cloud/bigtable": {
8895
"optional": true
8996
},
97+
"@google-cloud/firestore": {
98+
"optional": true
99+
},
90100
"@google-cloud/storage": {
91101
"optional": true
92102
}
@@ -100,6 +110,10 @@
100110
"import": "./dist/big-table.js",
101111
"require": "./dist/big-table.cjs"
102112
},
113+
"./firestore": {
114+
"import": "./dist/firestore.js",
115+
"require": "./dist/firestore.cjs"
116+
},
103117
"./storage": {
104118
"import": "./dist/storage.js",
105119
"require": "./dist/storage.cjs"

packages/gcp/src/big-table.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@ const COLUMN_FAMILY: ColumnFamily = "cf1";
1717
export class GcpBigtableDatabaseService implements DatabaseService {
1818
#instance: Instance;
1919

20-
constructor(options: BigtableOptions, instanceId: string = SERVICE_NAME) {
21-
this.#instance = new Bigtable(options).instance(instanceId);
20+
constructor(client: Bigtable, instanceId?: string);
21+
constructor(options: BigtableOptions, instanceId?: string);
22+
constructor(
23+
clientOrOptions: Bigtable | BigtableOptions,
24+
instanceId: string = SERVICE_NAME,
25+
) {
26+
const client =
27+
clientOrOptions instanceof Bigtable
28+
? clientOrOptions
29+
: new Bigtable(clientOrOptions);
30+
this.#instance = client.instance(instanceId);
2231
}
2332

2433
init: DatabaseService["init"] = async (_options) => {

packages/gcp/src/firestore.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Firestore, type Settings } from "@google-cloud/firestore";
2+
import type {
3+
DatabaseDocumentListOptions,
4+
DatabaseService,
5+
DatabaseServiceOptions,
6+
StoryBookerDatabaseDocument,
7+
} from "@storybooker/core/types";
8+
9+
export class GcpFirestoreDatabaseService implements DatabaseService {
10+
#instance: Firestore;
11+
12+
constructor(instance: Firestore);
13+
constructor(settings: Settings);
14+
constructor(instanceOrSettings: Firestore | Settings) {
15+
this.#instance =
16+
instanceOrSettings instanceof Firestore
17+
? instanceOrSettings
18+
: new Firestore(instanceOrSettings);
19+
}
20+
21+
listCollections: DatabaseService["listCollections"] = async (_options) => {
22+
const collections = await this.#instance.listCollections();
23+
return collections.map((col) => col.id);
24+
};
25+
26+
// oxlint-disable-next-line class-methods-use-this --- NOOP
27+
createCollection: DatabaseService["createCollection"] = async (
28+
_collectionId,
29+
_options,
30+
) => {
31+
// Firestore creates collections implicitly when you add a document.
32+
};
33+
34+
hasCollection: DatabaseService["hasCollection"] = async (
35+
collectionId,
36+
_options,
37+
) => {
38+
const col = this.#instance.collection(collectionId);
39+
const snapshot = await col.limit(1).get();
40+
return !snapshot.empty;
41+
};
42+
43+
deleteCollection: DatabaseService["deleteCollection"] = async (
44+
collectionId,
45+
_options,
46+
) => {
47+
// Firestore doesn't have a direct way to delete a collection
48+
// We need to delete all documents in the collection
49+
const col = this.#instance.collection(collectionId);
50+
const snapshot = await col.get();
51+
if (snapshot.empty) {
52+
return;
53+
}
54+
const batch = this.#instance.batch();
55+
for (const doc of snapshot.docs) {
56+
batch.delete(doc.ref);
57+
}
58+
await batch.commit();
59+
};
60+
61+
listDocuments: DatabaseService["listDocuments"] = async <
62+
Document extends StoryBookerDatabaseDocument,
63+
>(
64+
collectionId: string,
65+
_listOptions: DatabaseDocumentListOptions<Document>,
66+
_options: DatabaseServiceOptions,
67+
) => {
68+
const col = this.#instance.collection(collectionId);
69+
const snapshot = await col.get();
70+
const list: Document[] = [];
71+
for (const doc of snapshot.docs) {
72+
const data = doc.data() as Document;
73+
list.push({ ...data, id: doc.id });
74+
}
75+
76+
return list;
77+
};
78+
79+
getDocument: DatabaseService["getDocument"] = async <
80+
Document extends StoryBookerDatabaseDocument,
81+
>(
82+
collectionId: string,
83+
documentId: string,
84+
_options: DatabaseServiceOptions,
85+
) => {
86+
const docRef = this.#instance.collection(collectionId).doc(documentId);
87+
const doc = await docRef.get();
88+
if (!doc.exists) {
89+
throw new Error(`Document '${documentId}' not found.`);
90+
}
91+
return { ...doc.data(), id: doc.id } as Document;
92+
};
93+
94+
createDocument: DatabaseService["createDocument"] = async (
95+
collectionId,
96+
documentData,
97+
_options,
98+
) => {
99+
const docRef = this.#instance.collection(collectionId).doc(documentData.id);
100+
await docRef.create(documentData);
101+
};
102+
103+
hasDocument: DatabaseService["hasDocument"] = async (
104+
collectionId,
105+
documentId,
106+
_options,
107+
) => {
108+
const docRef = this.#instance.collection(collectionId).doc(documentId);
109+
const doc = await docRef.get();
110+
return doc.exists;
111+
};
112+
113+
deleteDocument: DatabaseService["deleteDocument"] = async (
114+
collectionId,
115+
documentId,
116+
_options,
117+
) => {
118+
const docRef = this.#instance.collection(collectionId).doc(documentId);
119+
await docRef.delete();
120+
};
121+
122+
updateDocument: DatabaseService["updateDocument"] = async (
123+
collectionId,
124+
documentId,
125+
documentData,
126+
) => {
127+
const docRef = this.#instance.collection(collectionId).doc(documentId);
128+
await docRef.set(documentData, {
129+
merge: true,
130+
mergeFields: Object.keys(documentData),
131+
});
132+
};
133+
}

packages/gcp/src/storage.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ import type { StorageService } from "@storybooker/core/types";
66
export class GcpGcsStorageService implements StorageService {
77
#client: Storage;
88

9-
constructor(options: StorageOptions) {
10-
this.#client = new Storage(options);
9+
constructor(client: Storage);
10+
constructor(options: StorageOptions);
11+
constructor(clientOrOptions: Storage | StorageOptions) {
12+
this.#client =
13+
clientOrOptions instanceof Storage
14+
? clientOrOptions
15+
: new Storage(clientOrOptions);
1116
}
1217

1318
createContainer: StorageService["createContainer"] = async (

packages/gcp/tsdown.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineConfig } from "tsdown";
22

33
export default defineConfig({
44
dts: { tsgo: true },
5-
entry: ["./src/big-table.ts", "./src/storage.ts"],
5+
entry: ["./src/big-table.ts", "./src/firestore.ts", "./src/storage.ts"],
66
exports: { devExports: "source" },
77
format: ["esm", "cjs"],
88
platform: "node",

0 commit comments

Comments
 (0)