@firestore-repository/google-cloud-firestore 0.4.1 → 0.5.0

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/README.md CHANGED
@@ -4,13 +4,14 @@
4
4
 
5
5
  # firestore-repository
6
6
 
7
- A minimum and universal Firestore ORM (Repository Pattern) for TypeScript
7
+ A minimal and universal Firestore client (Repository Pattern) for TypeScript
8
8
 
9
9
  ## Features
10
10
 
11
- - 🚀 **Minimum**: Only a few straightforward interfaces and classes. You can easily start to use it immediately without learning a lot of things.
12
- - 🌐 **Unopinionated**: This library does not introduce any additional concepts, and respects an interface of the official Firestore client library.
13
- - **Type-safe**: This library provides the type-safe interface. It also covers the untyped parts of the official Firestore library.
11
+ - 🚀 **Minimal**: Only a few straightforward interfaces and classes. You can start using it immediately without a steep learning curve.
12
+ - 🌐 **Universal**: You can share most code, including schema and query definitions, between backend and frontend.
13
+ - 🤝 **Unopinionated**: This library does not introduce any additional concepts, and respects the vocabulary of the official Firestore client library.
14
+ - ✅ **Type-safe**: This library provides a type-safe interface. It also covers the untyped parts of the official Firestore library.
14
15
  - 🗄️ **Repository Pattern**: A simple and consistent way to access Firestore data.
15
16
 
16
17
  ## Installation
@@ -18,8 +19,8 @@ A minimum and universal Firestore ORM (Repository Pattern) for TypeScript
18
19
  ### For backend (with [`@google-cloud/firestore`](https://www.npmjs.com/package/@google-cloud/firestore))
19
20
 
20
21
  ```shell
21
- npm install firestore-repository @firestore-repository/google-cloud-firestore
22
- ````
22
+ npm install firestore-repository @firestore-repository/google-cloud-firestore
23
+ ```
23
24
 
24
25
  ### For web frontend (with [`@firebase/firestore`](https://www.npmjs.com/package/@firebase/firestore))
25
26
 
@@ -32,86 +33,100 @@ npm install firestore-repository @firestore-repository/firebase-js-sdk
32
33
  ### Define a collection and its repository
33
34
 
34
35
  ```ts
35
- import { mapTo, data, rootCollection } from 'firestore-repository/schema';
36
+ import {
37
+ rootCollection,
38
+ string,
39
+ double,
40
+ map,
41
+ optional,
42
+ literal,
43
+ array,
44
+ } from 'firestore-repository/schema';
36
45
 
37
46
  // For backend
38
47
  import { Firestore } from '@google-cloud/firestore';
39
- import { Repository } from '@firestore-repository/google-cloud-firestore';
48
+ import { rootCollectionRepository } from '@firestore-repository/google-cloud-firestore';
40
49
  const db = new Firestore();
41
50
 
42
51
  // For web frontend
43
52
  import { getFirestore } from '@firebase/firestore';
44
- import { Repository } from '@firestore-repository/firebase-js-sdk';
53
+ import { rootCollectionRepository } from '@firestore-repository/firebase-js-sdk';
45
54
  const db = getFirestore();
46
55
 
47
56
  // define a collection
48
57
  const users = rootCollection({
49
58
  name: 'Users',
50
- id: mapTo('userId'),
51
- data: data<{
52
- name: string;
53
- profile: {
54
- age: number;
55
- gender?: 'male' | 'female';
56
- };
57
- tag: string[];
58
- }>(),
59
+ schema: {
60
+ name: string(),
61
+ profile: map({ age: double(), gender: optional(literal('male', 'female')) }),
62
+ tag: array(string()),
63
+ },
59
64
  });
60
65
 
61
- const repository = new Repository(users, db);
66
+ const repository = rootCollectionRepository(db, users);
62
67
  ```
63
68
 
64
69
  ### Basic operations for a single document
65
70
 
71
+ All operations are **type-safe** based on the schema you defined. The `data` field is typed according to your schema, so invalid data structures are caught at compile time.
72
+
66
73
  ```ts
67
74
  // Set a document
68
75
  await repository.set({
69
- userId: 'user1',
70
- name: 'John Doe',
71
- profile: {
72
- age: 42,
73
- gender: 'male',
74
- },
75
- tag: ['new'],
76
+ ref: 'user1',
77
+ data: { name: 'John Doe', profile: { age: 42, gender: 'male' }, tag: ['new'] },
78
+ });
79
+
80
+ // Create a document (backend only)
81
+ await repository.create({
82
+ ref: 'user2',
83
+ data: { name: 'Charlie', profile: { age: 25, gender: 'male' }, tag: [] },
76
84
  });
77
85
 
78
86
  // Get a document
79
- const doc = await repository.get({ userId: 'user1' });
87
+ const doc = await repository.get('user1');
80
88
 
81
- // Listen a document
82
- repository.getOnSnapshot({ userId: 'user1' }, (doc) => {
89
+ // Listen to a document
90
+ repository.getOnSnapshot('user1', (doc) => {
83
91
  console.log(doc);
84
92
  });
85
93
 
86
94
  // Delete a document
87
- await repository.delete({ userId: 'user2' });
95
+ await repository.delete('user2');
88
96
  ```
89
97
 
90
98
  ### Query
91
99
 
100
+ Field paths in query conditions are **automatically derived from the schema type**, not just plain strings — so typos and invalid paths are caught at compile time. The filter value is also **type-checked based on the field type and operator** (e.g., `array-contains` expects an element type of the array field).
101
+
92
102
  ```ts
93
- import { condition as $, limit, query, where } from 'firestore-repository/query';
103
+ import { eq, gte, limit, query, where } from 'firestore-repository/query';
104
+ import { average, count, sum } from 'firestore-repository/aggregate';
94
105
 
95
106
  // Define a query
96
- const query1 = query(users, $('profile.age', '>=', 20), limit(10));
107
+ // Field paths like 'profile.age' are auto-completed and type-checked against the schema.
108
+ // The value `20` is validated as `number` because `profile.age` is `number`.
109
+ const q = query(
110
+ { collection: users },
111
+ where(gte('profile.age', 20), eq('profile.gender', 'male')),
112
+ // where(gte('profile.age', 'foo')) // ← Compile error: string is not assignable to number
113
+ // where(eq('nonExistent', 1)) // ← Compile error: invalid field path
114
+ limit(10),
115
+ );
97
116
 
98
117
  // List documents
99
- const docs = await repository.list(query1);
100
- console.log(docs);
118
+ const docs = await repository.list(q);
101
119
 
102
- // Listen documents
103
- repository.listOnSnapshot(query1, (docs) => {
120
+ // Listen to documents
121
+ repository.listOnSnapshot(q, (docs) => {
104
122
  console.log(docs);
105
123
  });
106
124
 
107
125
  // Aggregate
108
- const result = await repository.aggregate({
109
- query: query1,
110
- spec: {
111
- avgAge: average('profile.age'),
112
- sumAge: sum('profile.age'),
113
- count: count(),
114
- },
126
+ const result = await repository.aggregate(q, {
127
+ avgAge: average('profile.age'),
128
+ sumAge: sum('profile.age'),
129
+ count: count(),
115
130
  });
116
131
  console.log(`avg:${result.avgAge} sum:${result.sumAge} count:${result.count}`);
117
132
  ```
@@ -120,52 +135,39 @@ console.log(`avg:${result.avgAge} sum:${result.sumAge} count:${result.count}`);
120
135
 
121
136
  ```ts
122
137
  // Get multiple documents (backend only)
123
- const users = await repository.batchGet([{ userId: 'user1' }, { userId: 'user2' }]);
138
+ const users = await repository.batchGet(['user1', 'user2']);
124
139
 
125
140
  // Set multiple documents
126
141
  await repository.batchSet([
127
- {
128
- userId: 'user1',
129
- name: 'Alice',
130
- profile: { age: 30, gender: 'female' },
131
- tag: ['new'],
132
- },
133
- {
134
- userId: 'user2',
135
- name: 'Bob',
136
- profile: { age: 20, gender: 'male' },
137
- tag: [],
138
- },
142
+ { ref: 'user1', data: { name: 'Alice', profile: { age: 30, gender: 'female' }, tag: ['new'] } },
143
+ { ref: 'user2', data: { name: 'Bob', profile: { age: 20, gender: 'male' }, tag: [] } },
139
144
  ]);
140
145
 
141
146
  // Delete multiple documents
142
- await repository.batchDelete([{ userId: 'user1' }, { userId: 'user2' }]);
147
+ await repository.batchDelete(['user1', 'user2']);
143
148
  ```
144
149
 
145
150
  #### Include multiple different operations in a batch
146
151
 
147
152
  ```ts
148
153
  // For backend
149
- const batch = db.writeBatch();
154
+ const batch = db.batch();
150
155
  // For web frontend
151
156
  import { writeBatch } from '@firebase/firestore';
152
- const batch = writeBatch();
157
+ const batch = writeBatch(db);
153
158
 
154
159
  await repository.set(
155
- {
156
- userId: 'user3',
157
- name: 'Bob',
158
- profile: { age: 20, gender: 'male' },
159
- tag: [],
160
- },
161
- { tx: batch },
160
+ { ref: 'user3', data: { name: 'Bob', profile: { age: 20, gender: 'male' }, tag: [] } },
161
+ { tx: batch },
162
162
  );
163
- await repository.batchSet([ /* ... */ ], { tx: batch },
163
+ await repository.batchSet(
164
+ [
165
+ /* ... */
166
+ ],
167
+ { tx: batch },
164
168
  );
165
- await repository.delete({ userId: 'user4' }, { tx: batch });
166
- await repository.batchDelete([{ userId: 'user5' }, { userId: 'user6' }], {
167
- tx: batch,
168
- });
169
+ await repository.delete('user4', { tx: batch });
170
+ await repository.batchDelete(['user5', 'user6'], { tx: batch });
169
171
 
170
172
  await batch.commit();
171
173
  ```
@@ -176,24 +178,105 @@ await batch.commit();
176
178
  // For web frontend
177
179
  import { runTransaction } from '@firebase/firestore';
178
180
 
179
- // Or, please use db.runTransaction for backend
180
- await runTransaction(async (tx) => {
181
+ // Or use db.runTransaction for backend
182
+ await runTransaction(db, async (tx) => {
181
183
  // Get
182
- const doc = await repository.get({ userId: 'user1' }, { tx });
183
-
184
+ const doc = await repository.get('user1', { tx });
185
+
184
186
  if (doc) {
185
- doc.tag = [...doc.tag, 'new-tag'];
187
+ doc.data.tag = [...doc.data.tag, 'new-tag'];
186
188
  // Set
187
189
  await repository.set(doc, { tx });
188
- await repository.batchSet([
189
- { ...doc, userId: 'user2' },
190
- { ...doc, userId: 'user3' },
191
- ]);
190
+ await repository.batchSet(
191
+ [
192
+ { ...doc, ref: 'user2' },
193
+ { ...doc, ref: 'user3' },
194
+ ],
195
+ { tx },
196
+ );
192
197
  }
193
198
 
194
199
  // Delete
195
- await repository.delete({ userId: 'user4' }, { tx });
196
- await repository.batchDelete([{ userId: 'user5' }, { userId: 'user6' }]);
200
+ await repository.delete('user4', { tx });
201
+ await repository.batchDelete(['user5', 'user6'], { tx });
202
+ });
203
+ ```
204
+
205
+ ### Subcollection
206
+
207
+ Subcollections are defined with `subCollection`, specifying the parent collection path. The only difference from root collections is that the document ref becomes a tuple (array of parent doc ID + doc ID). All other operations (query, batch, transaction, etc.) work the same.
208
+
209
+ ```ts
210
+ import { subCollection, string } from 'firestore-repository/schema';
211
+
212
+ // For backend
213
+ import { subcollectionRepository } from '@firestore-repository/google-cloud-firestore';
214
+
215
+ // For web frontend
216
+ import { subcollectionRepository } from '@firestore-repository/firebase-js-sdk';
217
+
218
+ const posts = subCollection({
219
+ name: 'Posts',
220
+ schema: { title: string() },
221
+ parent: ['Users'] as const,
197
222
  });
223
+
224
+ const postRepository = subcollectionRepository(db, posts);
225
+
226
+ // Set a document (ref is [parentDocId, docId])
227
+ await postRepository.set({ ref: ['user1', 'post1'], data: { title: 'My first post' } });
228
+
229
+ // Get a document
230
+ const post = await postRepository.get(['user1', 'post1']);
198
231
  ```
199
232
 
233
+ ### Custom Mapper
234
+
235
+ By default, `rootCollectionRepository` returns a repository with `{ ref: string, data: ... }` as its model type. If you want to use your own application model types, you can define a custom `Mapper` and use `repositoryWithMapper` to create a repository that automatically converts between Firestore documents and your models.
236
+
237
+ A `Mapper` consists of three functions:
238
+
239
+ - `toDocRef`: Converts your model's ID to a Firestore document reference
240
+ - `fromFirestore`: Converts a Firestore document to your read model
241
+ - `toFirestore`: Converts your write model to a Firestore document
242
+
243
+ You can also define different types for reading and writing via `AppModel<Id, Read, Write>` (e.g., omitting server-managed fields from the write type).
244
+
245
+ ```ts
246
+ import { type AppModel, type Mapper } from 'firestore-repository/repository';
247
+
248
+ // For backend
249
+ import { repositoryWithMapper } from '@firestore-repository/google-cloud-firestore';
250
+ // For web frontend
251
+ import { repositoryWithMapper } from '@firestore-repository/firebase-js-sdk';
252
+
253
+ // Define your application model type
254
+ type User = {
255
+ id: string;
256
+ name: string;
257
+ profile: { age: number; gender?: 'male' | 'female' };
258
+ tag: string[];
259
+ };
260
+
261
+ // Define a mapper
262
+ const userMapper: Mapper<typeof users, AppModel<string, User, User>> = {
263
+ toDocRef: (id) => [id],
264
+ fromFirestore: (doc) => ({ id: doc.ref[0], ...doc.data }),
265
+ toFirestore: (user) => ({
266
+ ref: [user.id],
267
+ data: { name: user.name, profile: user.profile, tag: user.tag },
268
+ }),
269
+ };
270
+
271
+ const repository = repositoryWithMapper(db, users, userMapper);
272
+
273
+ // Now the repository accepts and returns your custom User type directly
274
+ await repository.set({
275
+ id: 'user1',
276
+ name: 'Alice',
277
+ profile: { age: 30, gender: 'female' },
278
+ tag: ['new'],
279
+ });
280
+ const user: User | undefined = await repository.get('user1');
281
+ await repository.delete('user1');
282
+ ```
@@ -0,0 +1,5 @@
1
+ import type * as firestore from '@google-cloud/firestore';
2
+ import type { DocumentSchema } from 'firestore-repository/schema';
3
+ import * as z from 'zod';
4
+ export declare function buildDecodeSchema(schema: DocumentSchema): z.ZodObject<z.ZodRawShape>;
5
+ export declare function buildEncodeSchema(schema: DocumentSchema, db: firestore.Firestore): z.ZodObject<z.ZodRawShape>;
@@ -0,0 +1,173 @@
1
+ import { DocumentReference as FirestoreDocumentReference, FieldValue, GeoPoint as FirestoreGeoPoint, Timestamp as FirestoreTimestamp, VectorValue as FirestoreVectorValue, } from '@google-cloud/firestore';
2
+ import { documentPath } from 'firestore-repository/path';
3
+ import { _optional, serverOperation } from 'firestore-repository/schema';
4
+ import { assertNever } from 'firestore-repository/util';
5
+ import * as z from 'zod';
6
+ const isServerOp = (v, op) => v != null && typeof v === 'object' && Reflect.get(v, serverOperation) === op;
7
+ export function buildDecodeSchema(schema) {
8
+ return z.object(Object.fromEntries(Object.entries(schema).map(([k, v]) => {
9
+ const s = buildDecodeField(v);
10
+ return [k, v[_optional] ? s.optional() : s];
11
+ })));
12
+ }
13
+ function buildDecodeField(fieldType) {
14
+ switch (fieldType.type) {
15
+ case 'string':
16
+ return z.string();
17
+ case 'bool':
18
+ return z.boolean();
19
+ case 'int64':
20
+ case 'double':
21
+ return z.number();
22
+ case 'null':
23
+ return z.null();
24
+ case 'bytes':
25
+ return z.instanceof(Buffer).transform((b) => new Uint8Array(b));
26
+ case 'timestamp':
27
+ return z.instanceof(FirestoreTimestamp).transform((ts) => ts.toDate());
28
+ case 'geoPoint':
29
+ return z
30
+ .instanceof(FirestoreGeoPoint)
31
+ .transform((gp) => ({ latitude: gp.latitude, longitude: gp.longitude }));
32
+ case 'vector':
33
+ // oxlint-disable-next-line typescript/no-explicit-any
34
+ return z
35
+ .custom((v) => v instanceof FirestoreVectorValue)
36
+ .transform((vv) => vv.toArray());
37
+ case 'docRef':
38
+ // oxlint-disable-next-line typescript/no-explicit-any
39
+ return z
40
+ .custom((v) => v instanceof FirestoreDocumentReference)
41
+ .transform((ref) => {
42
+ const ids = [];
43
+ let current = ref;
44
+ while (current != null) {
45
+ ids.push(current.id);
46
+ current = current.parent.parent;
47
+ }
48
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
49
+ return ids.reverse();
50
+ });
51
+ case 'map': {
52
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
53
+ const ft = fieldType;
54
+ return z.object(Object.fromEntries(Object.entries(ft.fields).map(([k, v]) => {
55
+ const s = buildDecodeField(v);
56
+ return [k, v[_optional] ? s.optional() : s];
57
+ })));
58
+ }
59
+ case 'array': {
60
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
61
+ const ft = fieldType;
62
+ return z.array(buildDecodeField(ft.dynamicPart));
63
+ }
64
+ case 'union': {
65
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
66
+ const ft = fieldType;
67
+ return zodUnion(ft.elements.map(buildDecodeField));
68
+ }
69
+ case 'const': {
70
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
71
+ const ft = fieldType;
72
+ return zodUnion(ft.values.map((v) => z.literal(v)));
73
+ }
74
+ default:
75
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
76
+ return assertNever(fieldType);
77
+ }
78
+ }
79
+ export function buildEncodeSchema(schema, db) {
80
+ return z.object(Object.fromEntries(Object.entries(schema).map(([k, v]) => {
81
+ const s = buildEncodeField(v, db);
82
+ return [k, v[_optional] ? s.optional() : s];
83
+ })));
84
+ }
85
+ function buildEncodeField(fieldType, db) {
86
+ switch (fieldType.type) {
87
+ case 'string':
88
+ return z.string();
89
+ case 'bool':
90
+ return z.boolean();
91
+ case 'null':
92
+ return z.null();
93
+ case 'bytes':
94
+ return z.instanceof(Uint8Array).transform((b) => Buffer.from(b));
95
+ case 'geoPoint':
96
+ return z
97
+ .object({ latitude: z.number(), longitude: z.number() })
98
+ .transform((gp) => new FirestoreGeoPoint(gp.latitude, gp.longitude));
99
+ case 'vector':
100
+ return z.array(z.number()).transform((arr) => FieldValue.vector(arr));
101
+ case 'int64':
102
+ case 'double':
103
+ return zodUnion([
104
+ // oxlint-disable-next-line typescript/no-explicit-any
105
+ z
106
+ .custom((v) => isServerOp(v, 'increment'))
107
+ .transform((v) => FieldValue.increment(v.amount)),
108
+ z.number(),
109
+ ]);
110
+ case 'timestamp':
111
+ return zodUnion([
112
+ // oxlint-disable-next-line typescript/no-explicit-any
113
+ z
114
+ .custom((v) => isServerOp(v, 'serverTimestamp'))
115
+ .transform(() => FieldValue.serverTimestamp()),
116
+ z.date().transform((d) => FirestoreTimestamp.fromDate(d)),
117
+ ]);
118
+ case 'docRef': {
119
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
120
+ const ft = fieldType;
121
+ return z.array(z.string()).transform((ref) =>
122
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
123
+ db.doc(documentPath(ft.collection, ref)));
124
+ }
125
+ case 'map': {
126
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
127
+ const ft = fieldType;
128
+ return z.object(Object.fromEntries(Object.entries(ft.fields).map(([k, v]) => {
129
+ const s = buildEncodeField(v, db);
130
+ return [k, v[_optional] ? s.optional() : s];
131
+ })));
132
+ }
133
+ case 'array': {
134
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
135
+ const ft = fieldType;
136
+ return zodUnion([
137
+ // oxlint-disable-next-line typescript/no-explicit-any
138
+ z
139
+ .custom((v) => isServerOp(v, 'arrayRemove'))
140
+ .transform((v) => FieldValue.arrayRemove(...v.values)),
141
+ // oxlint-disable-next-line typescript/no-explicit-any
142
+ z
143
+ .custom((v) => isServerOp(v, 'arrayUnion'))
144
+ .transform((v) => FieldValue.arrayUnion(...v.values)),
145
+ z.array(buildEncodeField(ft.dynamicPart, db)),
146
+ ]);
147
+ }
148
+ case 'union': {
149
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
150
+ const ft = fieldType;
151
+ return zodUnion(ft.elements.map((e) => buildEncodeField(e, db)));
152
+ }
153
+ case 'const': {
154
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
155
+ const ft = fieldType;
156
+ return zodUnion(ft.values.map((v) => z.literal(v)));
157
+ }
158
+ default:
159
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
160
+ return assertNever(fieldType);
161
+ }
162
+ }
163
+ function zodUnion(schemas) {
164
+ if (schemas.length === 0) {
165
+ throw new Error('union must have at least one element');
166
+ }
167
+ if (schemas.length === 1) {
168
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
169
+ return schemas[0];
170
+ }
171
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
172
+ return z.union(schemas);
173
+ }
@@ -1,59 +1,33 @@
1
- import { type DocumentReference, type DocumentSnapshot, Filter, type Firestore, type Query as FirestoreQuery, Transaction, type WriteBatch } from '@google-cloud/firestore';
2
- import type { AggregateSpec, Aggregated } from 'firestore-repository/aggregate';
3
- import type { WriteDocumentData } from 'firestore-repository/document';
4
- import type { FilterExpression, Offset, Query } from 'firestore-repository/query';
5
- import type * as repository from 'firestore-repository/repository';
6
- import { type CollectionSchema, type DocPathElement, type Id, type Model } from 'firestore-repository/schema';
1
+ import type * as firestore from '@google-cloud/firestore';
2
+ import { type AppModel, type Mapper, type PlainModel, type Repository, type RootCollectionPlainModel, type TransactionOption, type WriteTransactionOption } from 'firestore-repository/repository';
3
+ import type { Collection, RootCollection, SubCollection } from 'firestore-repository/schema';
4
+ /** Platform-specific environment types for Google Cloud Firestore */
7
5
  export type Env = {
8
- transaction: Transaction;
9
- writeBatch: WriteBatch;
10
- query: FirestoreQuery;
6
+ transaction: firestore.Transaction;
7
+ writeBatch: firestore.WriteBatch;
8
+ query: firestore.Query;
11
9
  };
12
- export type TransactionOption = repository.TransactionOption<Env>;
13
- export type WriteTransactionOption = repository.WriteTransactionOption<Env>;
14
- export declare class Repository<T extends CollectionSchema = CollectionSchema> implements repository.Repository<T, Env> {
15
- readonly collection: T;
16
- readonly db: Firestore;
17
- constructor(collection: T, db: Firestore);
18
- get(id: Id<T>, options?: TransactionOption): Promise<Model<T> | undefined>;
19
- getOnSnapshot(id: Id<T>, next: (snapshot: Model<T> | undefined) => void, error?: (error: Error) => void): repository.Unsubscribe;
20
- list(query: Query<T>): Promise<Model<T>[]>;
21
- listOnSnapshot(query: Query<T>, next: (snapshot: Model<T>[]) => void, error?: (error: Error) => void): repository.Unsubscribe;
22
- aggregate<U extends AggregateSpec<T>>(query: Query<T>, spec: U): Promise<Aggregated<U>>;
10
+ /** Extended repository interface for Google Cloud Firestore with additional methods (create, batchCreate, batchGet) */
11
+ export interface GoogleCloudFirestoreRepository<T extends Collection, Model extends AppModel> extends Repository<T, Model, Env> {
23
12
  /**
24
- * Create a new document
13
+ * Creates a new document
25
14
  * @throws If the document already exists
26
15
  */
27
- create(doc: Model<T>, options?: WriteTransactionOption): Promise<void>;
28
- set(doc: Model<T>, options?: WriteTransactionOption): Promise<void>;
29
- delete(id: Id<T>, options?: WriteTransactionOption): Promise<void>;
16
+ create: (docToWrite: Model['write'], options?: WriteTransactionOption<Env>) => Promise<void>;
30
17
  /**
31
- * Get documents by multiple ID
32
- * example: [{id:1},{id:2},{id:5},{id:1}] -> [doc1,doc2,undefined,doc1]
18
+ * Creates multiple documents.
19
+ * The entire operation fails if any creation fails.
33
20
  */
34
- batchGet(ids: Id<T>[], options?: TransactionOption): Promise<(Model<T> | undefined)[]>;
35
- batchSet(docs: Model<T>[], options?: WriteTransactionOption): Promise<void>;
21
+ batchCreate: (docs: Model['write'][], options?: WriteTransactionOption<Env>) => Promise<void>;
36
22
  /**
37
- * Create multiple documents
38
- * The entire operation will fail if one creation fails
23
+ * Gets multiple documents by their IDs.
24
+ * @example [{id:1}, {id:2}, {id:5}, {id:1}] -> [doc1, doc2, undefined, doc1]
39
25
  */
40
- batchCreate(docs: Model<T>[], options?: WriteTransactionOption): Promise<void>;
41
- batchDelete(ids: Id<T>[], options?: WriteTransactionOption): Promise<void>;
42
- protected batchWriteOperation<U>(targets: U[], runner: {
43
- batch: (batch: WriteBatch, target: U) => void;
44
- transaction: (transaction: Transaction, target: U) => void;
45
- }, options?: WriteTransactionOption): Promise<void>;
46
- protected docRef(id: Id<T>): DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
47
- protected fromFirestore(doc: DocumentSnapshot): Model<T> | undefined;
48
- protected toFirestoreData(data: Model<T>): WriteDocumentData;
26
+ batchGet: (refs: Model['id'][], options?: TransactionOption<Env>) => Promise<(Model['read'] | undefined)[]>;
49
27
  }
50
- /**
51
- * Obtain document path elements from DocumentReference
52
- */
53
- export declare const docPathElements: (doc: DocumentReference) => [DocPathElement, ...DocPathElement[]];
54
- export declare const toFirestoreQuery: (db: Firestore, query: Query) => FirestoreQuery;
55
- export declare const toFirestoreFilter: (expr: FilterExpression) => Filter;
56
- /**
57
- * A query offset constraint
58
- */
59
- export declare const offset: (offset: number) => Offset;
28
+ /** Creates a repository for a root collection using plain document types */
29
+ export declare const rootCollectionRepository: <T extends RootCollection>(db: firestore.Firestore, collection: T) => Repository<T, RootCollectionPlainModel<T>, Env>;
30
+ /** Creates a repository for a subcollection using plain document types */
31
+ export declare const subcollectionRepository: <T extends SubCollection>(db: firestore.Firestore, collection: T) => Repository<T, PlainModel<T>, Env>;
32
+ /** Creates a repository with a custom mapper for transforming between Firestore documents and application models */
33
+ export declare const repositoryWithMapper: <T extends Collection, Model extends AppModel>(db: firestore.Firestore, collection: T, mapper: Mapper<T, Model>) => GoogleCloudFirestoreRepository<T, Model>;
@@ -1,217 +1,236 @@
1
- import { AggregateField, Filter, Transaction, } from '@google-cloud/firestore';
2
- import { collectionPath, docPath, } from 'firestore-repository/schema';
1
+ import { AggregateField, Filter, Transaction } from '@google-cloud/firestore';
2
+ import { collectionPath, documentPath } from 'firestore-repository/path';
3
+ import { plainMapper, rootCollectionPlainMapper, } from 'firestore-repository/repository';
3
4
  import { assertNever } from 'firestore-repository/util';
4
- export class Repository {
5
- collection;
6
- db;
7
- constructor(collection, db) {
8
- this.collection = collection;
9
- this.db = db;
10
- }
11
- async get(id, options) {
12
- const doc = await (options?.tx ? options.tx.get(this.docRef(id)) : this.docRef(id).get());
13
- return this.fromFirestore(doc);
14
- }
15
- getOnSnapshot(id, next, error) {
16
- return this.docRef(id).onSnapshot((snapshot) => {
17
- next(this.fromFirestore(snapshot));
18
- }, error);
19
- }
20
- async list(query) {
21
- const { docs } = await toFirestoreQuery(this.db, query).get();
22
- return docs.map((doc) =>
23
- // biome-ignore lint/style/noNonNullAssertion: Query result items should have data
24
- this.fromFirestore(doc));
25
- }
26
- listOnSnapshot(query, next, error) {
27
- return toFirestoreQuery(this.db, query).onSnapshot((snapshot) => {
28
- // biome-ignore lint/style/noNonNullAssertion: Query result items should have data
29
- next(snapshot.docs.map((doc) => this.fromFirestore(doc)));
30
- }, error);
31
- }
32
- async aggregate(query, spec) {
33
- const aggregateSpec = {};
34
- for (const [k, v] of Object.entries(spec)) {
35
- switch (v.kind) {
36
- case 'count':
37
- aggregateSpec[k] = AggregateField.count();
38
- break;
39
- case 'sum':
40
- aggregateSpec[k] = AggregateField.sum(v.path);
41
- break;
42
- case 'average':
43
- aggregateSpec[k] = AggregateField.average(v.path);
44
- break;
5
+ import { buildDecodeSchema, buildEncodeSchema } from './codec.js';
6
+ /** Creates a repository for a root collection using plain document types */
7
+ export const rootCollectionRepository = (db, collection) => repositoryWithMapper(db, collection, rootCollectionPlainMapper(collection));
8
+ /** Creates a repository for a subcollection using plain document types */
9
+ export const subcollectionRepository = (db, collection) => repositoryWithMapper(db, collection, plainMapper(collection));
10
+ /** Creates a repository with a custom mapper for transforming between Firestore documents and application models */
11
+ export const repositoryWithMapper = (db, collection, mapper) => {
12
+ const { toFirestore, fromFirestore, batchWriteOperation, encodeSchema } = buildFirestoreUtilities(db, collection);
13
+ // oxlint-disable-next-line typescript/no-explicit-any -- Zod output is passed to Firestore SDK
14
+ const encode = (data) => encodeSchema.parse(data);
15
+ return {
16
+ collection,
17
+ get: async (ref, options) => {
18
+ const docRef = toFirestore.docRef(mapper.toDocRef(ref));
19
+ const documentSnapshot = await (options?.tx ? options.tx.get(docRef) : docRef.get());
20
+ const doc = fromFirestore.document(documentSnapshot);
21
+ if (!doc) {
22
+ return undefined;
23
+ }
24
+ return mapper.fromFirestore(doc);
25
+ },
26
+ getOnSnapshot: (ref, next, error) => {
27
+ const docRef = toFirestore.docRef(mapper.toDocRef(ref));
28
+ return docRef.onSnapshot((snapshot) => {
29
+ const doc = fromFirestore.document(snapshot);
30
+ next(doc ? mapper.fromFirestore(doc) : undefined);
31
+ }, error);
32
+ },
33
+ list: async (query) => {
34
+ const firestoreQuery = toFirestore.query(query);
35
+ const { docs } = await firestoreQuery.get();
36
+ return docs.values().map((doc) => mapper.fromFirestore(fromFirestore.documentMustExist(doc)));
37
+ },
38
+ listOnSnapshot: (query, next, error) => {
39
+ const firestoreQuery = toFirestore.query(query);
40
+ return firestoreQuery.onSnapshot((snapshot) => {
41
+ next(snapshot.docs.map((doc) => mapper.fromFirestore(fromFirestore.documentMustExist(doc))));
42
+ }, error);
43
+ },
44
+ aggregate: async (query, spec) => {
45
+ const aggregateSpec = {};
46
+ for (const [k, v] of Object.entries(spec)) {
47
+ switch (v.kind) {
48
+ case 'count':
49
+ aggregateSpec[k] = AggregateField.count();
50
+ break;
51
+ case 'sum':
52
+ aggregateSpec[k] = AggregateField.sum(v.path);
53
+ break;
54
+ case 'average':
55
+ aggregateSpec[k] = AggregateField.average(v.path);
56
+ break;
57
+ default:
58
+ return assertNever(v);
59
+ }
60
+ }
61
+ const firestoreQuery = toFirestore.query(query);
62
+ const res = await firestoreQuery.aggregate(aggregateSpec).get();
63
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- there is no way to infer correct type
64
+ return res.data();
65
+ },
66
+ create: async (model, options) => {
67
+ const docToWrite = mapper.toFirestore(model);
68
+ const docRef = toFirestore.docRef(docToWrite.ref);
69
+ const data = encode(docToWrite.data);
70
+ await (options?.tx ? options.tx.create(docRef, data) : docRef.create(data));
71
+ },
72
+ set: async (model, options) => {
73
+ const docToWrite = mapper.toFirestore(model);
74
+ const docRef = toFirestore.docRef(docToWrite.ref);
75
+ const data = encode(docToWrite.data);
76
+ await (options?.tx
77
+ ? options.tx instanceof Transaction
78
+ ? options.tx.set(docRef, data)
79
+ : options.tx.set(docRef, data)
80
+ : docRef.set(data));
81
+ },
82
+ delete: async (ref, options) => {
83
+ const docRef = toFirestore.docRef(mapper.toDocRef(ref));
84
+ await (options?.tx ? options.tx.delete(docRef) : docRef.delete());
85
+ },
86
+ batchGet: async (refs, options) => {
87
+ if (refs.length === 0) {
88
+ return [];
89
+ }
90
+ const docRefs = refs.map((ref) => toFirestore.docRef(mapper.toDocRef(ref)));
91
+ const docs = await (options?.tx ? options.tx.getAll(...docRefs) : db.getAll(...docRefs));
92
+ return docs.map((doc) => {
93
+ const d = fromFirestore.document(doc);
94
+ return d ? mapper.fromFirestore(d) : undefined;
95
+ });
96
+ },
97
+ batchSet: async (models, options) => {
98
+ const docs = models.map((m) => {
99
+ const d = mapper.toFirestore(m);
100
+ return { ref: d.ref, data: encode(d.data) };
101
+ });
102
+ await batchWriteOperation(docs, {
103
+ batch: (batch, doc) => batch.set(toFirestore.docRef(doc.ref), doc.data),
104
+ transaction: (tx, doc) => tx.set(toFirestore.docRef(doc.ref), doc.data),
105
+ }, options);
106
+ },
107
+ batchCreate: async (models, options) => {
108
+ const docs = models.map((m) => {
109
+ const d = mapper.toFirestore(m);
110
+ return { ref: d.ref, data: encode(d.data) };
111
+ });
112
+ await batchWriteOperation(docs, {
113
+ batch: (batch, doc) => batch.create(toFirestore.docRef(doc.ref), doc.data),
114
+ transaction: (tx, doc) => tx.create(toFirestore.docRef(doc.ref), doc.data),
115
+ }, options);
116
+ },
117
+ batchDelete: async (refs, options) => {
118
+ const docRefs = refs.map(mapper.toDocRef);
119
+ await batchWriteOperation(docRefs, {
120
+ batch: (batch, ref) => batch.delete(toFirestore.docRef(ref)),
121
+ transaction: (tx, ref) => tx.delete(toFirestore.docRef(ref)),
122
+ }, options);
123
+ },
124
+ };
125
+ };
126
+ const buildFirestoreUtilities = (db, collection) => {
127
+ const decodeSchema = buildDecodeSchema(collection.schema);
128
+ const encodeSchema = buildEncodeSchema(collection.schema, db);
129
+ const toFirestore = {
130
+ docRef: (ref) => db.doc(documentPath(collection, ref)),
131
+ query: (query) => {
132
+ let base;
133
+ if ('collection' in query.base) {
134
+ base = query.base.group
135
+ ? db.collectionGroup(query.base.collection.name)
136
+ : db.collection(collectionPath(query.base.collection, query.base.parent));
137
+ }
138
+ else if ('extends' in query.base) {
139
+ base = toFirestore.query(query.base.extends);
140
+ }
141
+ else {
142
+ return assertNever(query.base);
143
+ }
144
+ return (query.constraints?.reduce((q, constraint) => {
145
+ switch (constraint.kind) {
146
+ case 'where':
147
+ return q.where(toFirestore.filter(constraint.condition));
148
+ case 'orderBy':
149
+ return q.orderBy(constraint.field, constraint.direction);
150
+ case 'limit':
151
+ return q.limit(constraint.limit);
152
+ case 'limitToLast':
153
+ return q.limitToLast(constraint.limit);
154
+ case 'offset':
155
+ return q.offset(constraint.offset);
156
+ case 'startAt': {
157
+ const { cursor } = constraint;
158
+ return q.startAt(...cursor);
159
+ }
160
+ case 'startAfter': {
161
+ const { cursor } = constraint;
162
+ return q.startAfter(...cursor);
163
+ }
164
+ case 'endAt': {
165
+ const { cursor } = constraint;
166
+ return q.endAt(...cursor);
167
+ }
168
+ case 'endBefore': {
169
+ const { cursor } = constraint;
170
+ return q.endBefore(...cursor);
171
+ }
172
+ default:
173
+ return assertNever(constraint);
174
+ }
175
+ }, base) ?? base);
176
+ },
177
+ filter: (expr) => {
178
+ switch (expr.kind) {
179
+ case 'fieldValueCondition':
180
+ return Filter.where(expr.fieldPath, expr.opStr, expr.value);
181
+ case 'and':
182
+ return Filter.and(...expr.filters.map(toFirestore.filter));
183
+ case 'or':
184
+ return Filter.or(...expr.filters.map(toFirestore.filter));
45
185
  default:
46
- return assertNever(v);
186
+ return assertNever(expr);
47
187
  }
48
- }
49
- const res = await toFirestoreQuery(this.db, query).aggregate(aggregateSpec).get();
50
- return res.data();
51
- }
52
- /**
53
- * Create a new document
54
- * @throws If the document already exists
55
- */
56
- async create(doc, options) {
57
- const data = this.toFirestoreData(doc);
58
- await (options?.tx ? options.tx.create(this.docRef(doc), data) : this.docRef(doc).create(data));
59
- }
60
- async set(doc, options) {
61
- const data = this.toFirestoreData(doc);
62
- await (options?.tx
63
- ? options.tx instanceof Transaction
64
- ? options.tx.set(this.docRef(doc), data)
65
- : options.tx.set(this.docRef(doc), data)
66
- : this.docRef(doc).set(data));
67
- }
68
- async delete(id, options) {
69
- await (options?.tx ? options.tx.delete(this.docRef(id)) : this.docRef(id).delete());
70
- }
71
- /**
72
- * Get documents by multiple ID
73
- * example: [{id:1},{id:2},{id:5},{id:1}] -> [doc1,doc2,undefined,doc1]
74
- */
75
- async batchGet(ids, options) {
76
- if (ids.length === 0) {
77
- return [];
78
- }
79
- const docRefs = ids.map((id) => this.docRef(id));
80
- const docs = await (options?.tx ? options.tx.getAll(...docRefs) : this.db.getAll(...docRefs));
81
- return docs.map((doc) => this.fromFirestore(doc));
82
- }
83
- async batchSet(docs, options) {
84
- await this.batchWriteOperation(docs, {
85
- batch: (batch, doc) => batch.set(this.docRef(doc), this.toFirestoreData(doc)),
86
- transaction: (tx, doc) => tx.set(this.docRef(doc), this.toFirestoreData(doc)),
87
- }, options);
88
- }
89
- /**
90
- * Create multiple documents
91
- * The entire operation will fail if one creation fails
92
- */
93
- async batchCreate(docs, options) {
94
- await this.batchWriteOperation(docs, {
95
- batch: (batch, doc) => batch.create(this.docRef(doc), this.toFirestoreData(doc)),
96
- transaction: (tx, doc) => tx.create(this.docRef(doc), this.toFirestoreData(doc)),
97
- }, options);
98
- }
99
- async batchDelete(ids, options) {
100
- await this.batchWriteOperation(ids, {
101
- batch: (batch, id) => batch.delete(this.docRef(id)),
102
- transaction: (tx, id) => tx.delete(this.docRef(id)),
103
- }, options);
104
- }
105
- async batchWriteOperation(targets, runner, options) {
188
+ },
189
+ };
190
+ const fromFirestore = {
191
+ documentMustExist: (document) => {
192
+ const data = document.data();
193
+ if (!data) {
194
+ throw new Error('document must exist');
195
+ }
196
+ return {
197
+ ref: fromFirestore.docRef(document.ref),
198
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Zod output is typed by schema
199
+ data: decodeSchema.parse(data),
200
+ };
201
+ },
202
+ document: (document) => {
203
+ if (!document.exists) {
204
+ return undefined;
205
+ }
206
+ return fromFirestore.documentMustExist(document);
207
+ },
208
+ docRef: (ref) => {
209
+ const docRef = [];
210
+ let currentRef = ref;
211
+ while (currentRef != null) {
212
+ docRef.push(currentRef.id);
213
+ currentRef = currentRef.parent.parent;
214
+ }
215
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- cannot infer type here
216
+ return docRef.reverse();
217
+ },
218
+ };
219
+ const batchWriteOperation = async (targets, runner, options) => {
106
220
  const tx = options?.tx;
107
221
  if (tx) {
108
222
  if (tx instanceof Transaction) {
109
- targets.forEach((target) => runner.transaction(tx, target));
223
+ targets.forEach((target) => void runner.transaction(tx, target));
110
224
  }
111
225
  else {
112
- targets.forEach((target) => runner.batch(tx, target));
226
+ targets.forEach((target) => void runner.batch(tx, target));
113
227
  }
114
228
  }
115
229
  else {
116
- const batch = this.db.batch();
117
- targets.forEach((target) => runner.batch(batch, target));
230
+ const batch = db.batch();
231
+ targets.forEach((target) => void runner.batch(batch, target));
118
232
  await batch.commit();
119
233
  }
120
- }
121
- docRef(id) {
122
- return this.db.doc(docPath(this.collection, id));
123
- }
124
- fromFirestore(doc) {
125
- const data = doc.data();
126
- const [id, ...parentPath] = docPathElements(doc.ref);
127
- return data
128
- ? {
129
- ...this.collection.data.from(data),
130
- ...this.collection.collectionPath.from(parentPath),
131
- ...this.collection.id.from(id.id),
132
- }
133
- : undefined;
134
- }
135
- toFirestoreData(data) {
136
- return this.collection.data.to(data);
137
- }
138
- }
139
- /**
140
- * Obtain document path elements from DocumentReference
141
- */
142
- export const docPathElements = (doc) => {
143
- const parentPath = [];
144
- let cursor = doc.parent.parent;
145
- while (cursor) {
146
- parentPath.push({ id: cursor.id, collection: cursor.parent.id });
147
- cursor = cursor.parent.parent;
148
- }
149
- return [{ collection: doc.parent.id, id: doc.id }, ...parentPath];
150
- };
151
- // OPTIMIZE: cache query
152
- export const toFirestoreQuery = (db, query) => {
153
- let base;
154
- switch (query.base.kind) {
155
- case 'collection':
156
- base = db.collection(collectionPath(query.base.collection, query.base.parentId));
157
- break;
158
- case 'collectionGroup':
159
- base = db.collectionGroup(query.base.collection.name);
160
- break;
161
- case 'extends':
162
- base = toFirestoreQuery(db, query.base.query);
163
- break;
164
- default:
165
- base = assertNever(query.base);
166
- }
167
- return (query.constraints?.reduce((q, constraint) => {
168
- switch (constraint.kind) {
169
- case 'where':
170
- case 'or':
171
- case 'and':
172
- return q.where(toFirestoreFilter(constraint));
173
- case 'orderBy':
174
- return q.orderBy(constraint.field, constraint.direction);
175
- case 'limit':
176
- return q.limit(constraint.limit);
177
- case 'limitToLast':
178
- return q.limitToLast(constraint.limit);
179
- case 'offset':
180
- return q.offset(constraint.offset);
181
- case 'startAt': {
182
- const { cursor } = constraint;
183
- return q.startAt(...cursor);
184
- }
185
- case 'startAfter': {
186
- const { cursor } = constraint;
187
- return q.startAfter(...cursor);
188
- }
189
- case 'endAt': {
190
- const { cursor } = constraint;
191
- return q.endAt(...cursor);
192
- }
193
- case 'endBefore': {
194
- const { cursor } = constraint;
195
- return q.endBefore(...cursor);
196
- }
197
- default:
198
- return assertNever(constraint);
199
- }
200
- }, base) ?? base);
201
- };
202
- export const toFirestoreFilter = (expr) => {
203
- switch (expr.kind) {
204
- case 'where':
205
- return Filter.where(expr.fieldPath, expr.opStr, expr.value);
206
- case 'and':
207
- return Filter.and(...expr.filters.map(toFirestoreFilter));
208
- case 'or':
209
- return Filter.or(...expr.filters.map(toFirestoreFilter));
210
- default:
211
- return assertNever(expr);
212
- }
234
+ };
235
+ return { fromFirestore, toFirestore, batchWriteOperation, encodeSchema };
213
236
  };
214
- /**
215
- * A query offset constraint
216
- */
217
- export const offset = (offset) => ({ kind: 'offset', offset });
@@ -0,0 +1,5 @@
1
+ import type { Offset } from 'firestore-repository/query';
2
+ /**
3
+ * Creates an offset constraint
4
+ */
5
+ export declare const offset: (offset: number) => Offset;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Creates an offset constraint
3
+ */
4
+ export const offset = (offset) => ({ kind: 'offset', offset });
package/package.json CHANGED
@@ -1,51 +1,56 @@
1
1
  {
2
2
  "name": "@firestore-repository/google-cloud-firestore",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "A minimum and universal Firestore ORM (Repository Pattern) for TypeScript",
5
+ "keywords": [
6
+ "database",
7
+ "firebase",
8
+ "firebase-admin",
9
+ "firestore",
10
+ "google-cloud",
11
+ "orm",
12
+ "repository"
13
+ ],
5
14
  "homepage": "https://github.com/ikenox/firestore-repository",
15
+ "license": "MIT",
16
+ "author": "Naoto Ikeno <ikenox@gmail.com>",
6
17
  "repository": {
7
18
  "type": "git",
8
19
  "url": "https://github.com/ikenox/firestore-repository.git"
9
20
  },
21
+ "files": [
22
+ "build",
23
+ "!**/*.tsbuildinfo"
24
+ ],
10
25
  "type": "module",
11
- "dependencies": {
12
- "@google-cloud/firestore": "^7.10.0",
13
- "firestore-repository": "0.4.1"
14
- },
15
26
  "exports": {
16
27
  ".": {
28
+ "@firestore-repository/source": "./src/index.ts",
17
29
  "import": {
18
30
  "types": "./build/esm/index.d.ts",
19
31
  "default": "./build/esm/index.js"
20
32
  }
21
33
  },
22
34
  "./*": {
35
+ "@firestore-repository/source": "./src/*.ts",
23
36
  "import": {
24
37
  "types": "./build/esm/*.d.ts",
25
38
  "default": "./build/esm/*.js"
26
39
  }
27
40
  }
28
41
  },
29
- "files": [
30
- "build",
31
- "!**/*.tsbuildinfo"
32
- ],
33
- "keywords": [
34
- "firestore",
35
- "orm",
36
- "database",
37
- "repository",
38
- "firebase",
39
- "firebase-admin",
40
- "google-cloud"
41
- ],
42
- "author": "Naoto Ikeno <ikenox@gmail.com>",
43
- "license": "MIT",
44
42
  "publishConfig": {
45
43
  "access": "public"
46
44
  },
45
+ "dependencies": {
46
+ "@google-cloud/firestore": "^8.3.0",
47
+ "zod": "^4.3.6",
48
+ "firestore-repository": "0.5.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
47
53
  "scripts": {
48
- "typecheck": "tsc --noEmit",
49
54
  "build": "rm -rf build/ && tsc -b tsconfig.build.json"
50
55
  }
51
56
  }