@lpdjs/firestore-repo-service 1.0.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/LICENSE +21 -0
- package/README.md +592 -0
- package/dist/index.cjs +755 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +420 -0
- package/dist/index.d.ts +420 -0
- package/dist/index.js +746 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// src/query-builder.ts
|
|
2
|
+
function chunkArray(array, size) {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
for (let i = 0; i < array.length; i += size) {
|
|
5
|
+
chunks.push(array.slice(i, i + size));
|
|
6
|
+
}
|
|
7
|
+
return chunks;
|
|
8
|
+
}
|
|
9
|
+
function applyBasicQueryOptions(q, options) {
|
|
10
|
+
if (options.orderBy) {
|
|
11
|
+
options.orderBy.forEach((o) => {
|
|
12
|
+
q = q.orderBy(String(o.field), o.direction || "asc");
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
if (options.limit) {
|
|
16
|
+
q = q.limit(options.limit);
|
|
17
|
+
}
|
|
18
|
+
if (options.offset) {
|
|
19
|
+
q = q.offset(options.offset);
|
|
20
|
+
}
|
|
21
|
+
if (options.startAt) {
|
|
22
|
+
q = Array.isArray(options.startAt) ? q.startAt(...options.startAt) : q.startAt(options.startAt);
|
|
23
|
+
}
|
|
24
|
+
if (options.startAfter) {
|
|
25
|
+
q = Array.isArray(options.startAfter) ? q.startAfter(...options.startAfter) : q.startAfter(options.startAfter);
|
|
26
|
+
}
|
|
27
|
+
if (options.endAt) {
|
|
28
|
+
q = Array.isArray(options.endAt) ? q.endAt(...options.endAt) : q.endAt(options.endAt);
|
|
29
|
+
}
|
|
30
|
+
if (options.endBefore) {
|
|
31
|
+
q = Array.isArray(options.endBefore) ? q.endBefore(...options.endBefore) : q.endBefore(options.endBefore);
|
|
32
|
+
}
|
|
33
|
+
return q;
|
|
34
|
+
}
|
|
35
|
+
function needsSplitting(clause) {
|
|
36
|
+
const { operator, value } = clause;
|
|
37
|
+
return (operator === "in" || operator === "array-contains-any") && Array.isArray(value) && value.length > 30;
|
|
38
|
+
}
|
|
39
|
+
function splitWhereClause(clause) {
|
|
40
|
+
const { field, operator, value } = clause;
|
|
41
|
+
if (!needsSplitting(clause)) {
|
|
42
|
+
return [clause];
|
|
43
|
+
}
|
|
44
|
+
const chunks = chunkArray(value, 30);
|
|
45
|
+
return chunks.map((chunk) => ({
|
|
46
|
+
field,
|
|
47
|
+
operator,
|
|
48
|
+
value: chunk
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function applyWhereClausesToQuery(baseQuery, whereClauses) {
|
|
52
|
+
let q = baseQuery;
|
|
53
|
+
for (const clause of whereClauses) {
|
|
54
|
+
q = q.where(String(clause.field), clause.operator, clause.value);
|
|
55
|
+
}
|
|
56
|
+
return q;
|
|
57
|
+
}
|
|
58
|
+
async function executeAndMergeQueries(queries) {
|
|
59
|
+
const snapshots = await Promise.all(queries.map((q) => q.get()));
|
|
60
|
+
const docsMap = /* @__PURE__ */ new Map();
|
|
61
|
+
snapshots.forEach((snapshot) => {
|
|
62
|
+
snapshot.docs.forEach((doc) => {
|
|
63
|
+
if (!docsMap.has(doc.id)) {
|
|
64
|
+
docsMap.set(doc.id, doc);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
const firstSnapshot = snapshots[0];
|
|
69
|
+
if (!firstSnapshot) {
|
|
70
|
+
throw new Error("No snapshots returned");
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...firstSnapshot,
|
|
74
|
+
docs: Array.from(docsMap.values()),
|
|
75
|
+
size: docsMap.size,
|
|
76
|
+
empty: docsMap.size === 0
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function buildAndExecuteQuery(baseQuery, options) {
|
|
80
|
+
if (options.where && !options.orWhere) {
|
|
81
|
+
const needsSplit = options.where.some(needsSplitting);
|
|
82
|
+
if (!needsSplit) {
|
|
83
|
+
let q2 = applyWhereClausesToQuery(baseQuery, options.where);
|
|
84
|
+
q2 = applyBasicQueryOptions(q2, options);
|
|
85
|
+
return q2.get();
|
|
86
|
+
}
|
|
87
|
+
const splitClauses = options.where.map(splitWhereClause);
|
|
88
|
+
const combinations = cartesianProduct(splitClauses);
|
|
89
|
+
const queries = combinations.map((combination) => {
|
|
90
|
+
let q2 = applyWhereClausesToQuery(baseQuery, combination);
|
|
91
|
+
q2 = applyBasicQueryOptions(q2, options);
|
|
92
|
+
return q2;
|
|
93
|
+
});
|
|
94
|
+
return executeAndMergeQueries(queries);
|
|
95
|
+
}
|
|
96
|
+
if (options.orWhere) {
|
|
97
|
+
const allQueries = [];
|
|
98
|
+
for (const orGroup of options.orWhere) {
|
|
99
|
+
const needsSplit = orGroup.some(needsSplitting);
|
|
100
|
+
if (!needsSplit) {
|
|
101
|
+
let q2 = applyWhereClausesToQuery(baseQuery, orGroup);
|
|
102
|
+
q2 = applyBasicQueryOptions(q2, options);
|
|
103
|
+
allQueries.push(q2);
|
|
104
|
+
} else {
|
|
105
|
+
const splitClauses = orGroup.map(splitWhereClause);
|
|
106
|
+
const combinations = cartesianProduct(splitClauses);
|
|
107
|
+
const groupQueries = combinations.map((combination) => {
|
|
108
|
+
let q2 = applyWhereClausesToQuery(baseQuery, combination);
|
|
109
|
+
q2 = applyBasicQueryOptions(q2, options);
|
|
110
|
+
return q2;
|
|
111
|
+
});
|
|
112
|
+
allQueries.push(...groupQueries);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return executeAndMergeQueries(allQueries);
|
|
116
|
+
}
|
|
117
|
+
const q = applyBasicQueryOptions(baseQuery, options);
|
|
118
|
+
return q.get();
|
|
119
|
+
}
|
|
120
|
+
function cartesianProduct(arrays) {
|
|
121
|
+
if (arrays.length === 0) return [[]];
|
|
122
|
+
const first = arrays[0];
|
|
123
|
+
if (arrays.length === 1 && first) {
|
|
124
|
+
return first.map((item) => [item]);
|
|
125
|
+
}
|
|
126
|
+
if (!first) return [[]];
|
|
127
|
+
const rest = arrays.slice(1);
|
|
128
|
+
const restProduct = cartesianProduct(rest);
|
|
129
|
+
const result = [];
|
|
130
|
+
for (const item of first) {
|
|
131
|
+
for (const combo of restProduct) {
|
|
132
|
+
result.push([item, ...combo]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/pagination.ts
|
|
139
|
+
function applyQueryOptions(q, options) {
|
|
140
|
+
if (options.where) {
|
|
141
|
+
options.where.forEach((w) => {
|
|
142
|
+
q = q.where(String(w.field), w.operator, w.value);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (options.orderBy) {
|
|
146
|
+
options.orderBy.forEach((o) => {
|
|
147
|
+
q = q.orderBy(String(o.field), o.direction || "asc");
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (options.limit) {
|
|
151
|
+
q = q.limit(options.limit);
|
|
152
|
+
}
|
|
153
|
+
if (options.offset) {
|
|
154
|
+
q = q.offset(options.offset);
|
|
155
|
+
}
|
|
156
|
+
if (options.startAt) {
|
|
157
|
+
q = Array.isArray(options.startAt) ? q.startAt(...options.startAt) : q.startAt(options.startAt);
|
|
158
|
+
}
|
|
159
|
+
if (options.startAfter) {
|
|
160
|
+
q = Array.isArray(options.startAfter) ? q.startAfter(...options.startAfter) : q.startAfter(options.startAfter);
|
|
161
|
+
}
|
|
162
|
+
if (options.endAt) {
|
|
163
|
+
q = Array.isArray(options.endAt) ? q.endAt(...options.endAt) : q.endAt(options.endAt);
|
|
164
|
+
}
|
|
165
|
+
if (options.endBefore) {
|
|
166
|
+
q = Array.isArray(options.endBefore) ? q.endBefore(...options.endBefore) : q.endBefore(options.endBefore);
|
|
167
|
+
}
|
|
168
|
+
return q;
|
|
169
|
+
}
|
|
170
|
+
async function executePaginatedQuery(baseQuery, options) {
|
|
171
|
+
const queryOptions = {
|
|
172
|
+
...options,
|
|
173
|
+
limit: options.pageSize + 1
|
|
174
|
+
// Fetch one extra to check if there's a next page
|
|
175
|
+
};
|
|
176
|
+
if (options.cursor) {
|
|
177
|
+
if (options.direction === "prev") {
|
|
178
|
+
queryOptions.endBefore = options.cursor;
|
|
179
|
+
} else {
|
|
180
|
+
queryOptions.startAfter = options.cursor;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const snapshot = await buildAndExecuteQuery(baseQuery, queryOptions);
|
|
184
|
+
const docs = snapshot.docs;
|
|
185
|
+
const hasMore = docs.length > options.pageSize;
|
|
186
|
+
const actualDocs = hasMore ? docs.slice(0, options.pageSize) : docs;
|
|
187
|
+
const data = actualDocs.map((doc) => ({
|
|
188
|
+
...doc.data(),
|
|
189
|
+
docId: doc.id
|
|
190
|
+
}));
|
|
191
|
+
return {
|
|
192
|
+
data,
|
|
193
|
+
nextCursor: hasMore ? actualDocs[actualDocs.length - 1] : void 0,
|
|
194
|
+
prevCursor: actualDocs[0],
|
|
195
|
+
hasNextPage: hasMore,
|
|
196
|
+
hasPrevPage: !!options.cursor,
|
|
197
|
+
pageSize: data.length
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function* createPaginationIterator(baseQuery, options) {
|
|
201
|
+
let cursor;
|
|
202
|
+
let hasMore = true;
|
|
203
|
+
while (hasMore) {
|
|
204
|
+
const result = await executePaginatedQuery(baseQuery, {
|
|
205
|
+
...options,
|
|
206
|
+
cursor,
|
|
207
|
+
direction: "next"
|
|
208
|
+
});
|
|
209
|
+
yield result;
|
|
210
|
+
hasMore = result.hasNextPage;
|
|
211
|
+
cursor = result.nextCursor;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/shared/utils.ts
|
|
216
|
+
function chunkArray2(array, size) {
|
|
217
|
+
const chunks = [];
|
|
218
|
+
for (let i = 0; i < array.length; i += size) {
|
|
219
|
+
chunks.push(array.slice(i, i + size));
|
|
220
|
+
}
|
|
221
|
+
return chunks;
|
|
222
|
+
}
|
|
223
|
+
function capitalize(str) {
|
|
224
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/methods/query.ts
|
|
228
|
+
function applyQueryOptions2(q, options) {
|
|
229
|
+
if (options.where) {
|
|
230
|
+
options.where.forEach((w) => {
|
|
231
|
+
q = q.where(String(w.field), w.operator, w.value);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (options.orderBy) {
|
|
235
|
+
options.orderBy.forEach((o) => {
|
|
236
|
+
q = q.orderBy(String(o.field), o.direction || "asc");
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (options.limit) {
|
|
240
|
+
q = q.limit(options.limit);
|
|
241
|
+
}
|
|
242
|
+
if (options.offset) {
|
|
243
|
+
q = q.offset(options.offset);
|
|
244
|
+
}
|
|
245
|
+
if (options.startAt) {
|
|
246
|
+
q = Array.isArray(options.startAt) ? q.startAt(...options.startAt) : q.startAt(options.startAt);
|
|
247
|
+
}
|
|
248
|
+
if (options.startAfter) {
|
|
249
|
+
q = Array.isArray(options.startAfter) ? q.startAfter(...options.startAfter) : q.startAfter(options.startAfter);
|
|
250
|
+
}
|
|
251
|
+
if (options.endAt) {
|
|
252
|
+
q = Array.isArray(options.endAt) ? q.endAt(...options.endAt) : q.endAt(options.endAt);
|
|
253
|
+
}
|
|
254
|
+
if (options.endBefore) {
|
|
255
|
+
q = Array.isArray(options.endBefore) ? q.endBefore(...options.endBefore) : q.endBefore(options.endBefore);
|
|
256
|
+
}
|
|
257
|
+
return q;
|
|
258
|
+
}
|
|
259
|
+
function createQueryMethods(collectionRef, queryKeys) {
|
|
260
|
+
const queryMethods = {};
|
|
261
|
+
queryKeys.forEach((queryKey) => {
|
|
262
|
+
const methodName = `by${capitalize(String(queryKey))}`;
|
|
263
|
+
queryMethods[methodName] = async (value, options = {}) => {
|
|
264
|
+
let q = collectionRef;
|
|
265
|
+
q = q.where(String(queryKey), "==", value);
|
|
266
|
+
q = applyQueryOptions2(q, options);
|
|
267
|
+
const snapshot = await q.get();
|
|
268
|
+
return snapshot.docs.map((doc) => ({ ...doc.data(), docId: doc.id }));
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
queryMethods.by = async (options) => {
|
|
272
|
+
let q = collectionRef;
|
|
273
|
+
q = applyQueryOptions2(q, options);
|
|
274
|
+
const snapshot = await q.get();
|
|
275
|
+
return snapshot.docs.map((doc) => ({ ...doc.data(), docId: doc.id }));
|
|
276
|
+
};
|
|
277
|
+
queryMethods.getAll = async (options = {}) => {
|
|
278
|
+
let q = collectionRef;
|
|
279
|
+
q = applyQueryOptions2(q, options);
|
|
280
|
+
const snapshot = await q.get();
|
|
281
|
+
return snapshot.docs.map((doc) => ({ ...doc.data(), docId: doc.id }));
|
|
282
|
+
};
|
|
283
|
+
queryMethods.onSnapshot = (options, onNext, onError) => {
|
|
284
|
+
let q = collectionRef;
|
|
285
|
+
q = applyQueryOptions2(q, options);
|
|
286
|
+
return q.onSnapshot((snapshot) => {
|
|
287
|
+
const data = snapshot.docs.map((doc) => ({
|
|
288
|
+
...doc.data(),
|
|
289
|
+
docId: doc.id
|
|
290
|
+
}));
|
|
291
|
+
onNext(data);
|
|
292
|
+
}, onError);
|
|
293
|
+
};
|
|
294
|
+
queryMethods.paginate = async (options) => {
|
|
295
|
+
return executePaginatedQuery(collectionRef, options);
|
|
296
|
+
};
|
|
297
|
+
queryMethods.paginateAll = (options) => {
|
|
298
|
+
return createPaginationIterator(collectionRef, options);
|
|
299
|
+
};
|
|
300
|
+
return queryMethods;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/methods/aggregate.ts
|
|
304
|
+
function createAggregateMethods(collectionRef) {
|
|
305
|
+
return {
|
|
306
|
+
// Count documents matching query options
|
|
307
|
+
count: async (options = {}) => {
|
|
308
|
+
let q = collectionRef;
|
|
309
|
+
q = applyQueryOptions2(q, options);
|
|
310
|
+
const snapshot = await q.count().get();
|
|
311
|
+
return snapshot.data().count;
|
|
312
|
+
},
|
|
313
|
+
// Sum of a numeric field
|
|
314
|
+
sum: async (field, options = {}) => {
|
|
315
|
+
let q = collectionRef;
|
|
316
|
+
q = applyQueryOptions2(q, options);
|
|
317
|
+
const snapshot = await q.get();
|
|
318
|
+
let total = 0;
|
|
319
|
+
snapshot.forEach((doc) => {
|
|
320
|
+
const value = doc.data()[field];
|
|
321
|
+
if (typeof value === "number") {
|
|
322
|
+
total += value;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
return total;
|
|
326
|
+
},
|
|
327
|
+
// Average of a numeric field
|
|
328
|
+
average: async (field, options = {}) => {
|
|
329
|
+
let q = collectionRef;
|
|
330
|
+
q = applyQueryOptions2(q, options);
|
|
331
|
+
const snapshot = await q.get();
|
|
332
|
+
if (snapshot.empty) return null;
|
|
333
|
+
let total = 0;
|
|
334
|
+
let count = 0;
|
|
335
|
+
snapshot.forEach((doc) => {
|
|
336
|
+
const value = doc.data()[field];
|
|
337
|
+
if (typeof value === "number") {
|
|
338
|
+
total += value;
|
|
339
|
+
count++;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return count > 0 ? total / count : null;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/methods/batch.ts
|
|
348
|
+
function createBatchMethods(db, documentRef) {
|
|
349
|
+
return {
|
|
350
|
+
create: () => {
|
|
351
|
+
const batch = db.batch();
|
|
352
|
+
return {
|
|
353
|
+
batch,
|
|
354
|
+
set: (...args) => {
|
|
355
|
+
const lastArg = args[args.length - 1];
|
|
356
|
+
const hasOptions = typeof lastArg === "object" && lastArg !== null && "merge" in lastArg;
|
|
357
|
+
const data = hasOptions ? args[args.length - 2] : args[args.length - 1];
|
|
358
|
+
const pathArgs = hasOptions ? args.slice(0, -2) : args.slice(0, -1);
|
|
359
|
+
const mergeOption = hasOptions ? lastArg : { merge: true };
|
|
360
|
+
const docRef = documentRef(...pathArgs);
|
|
361
|
+
batch.set(docRef, data, mergeOption);
|
|
362
|
+
},
|
|
363
|
+
update: (...args) => {
|
|
364
|
+
const data = args.pop();
|
|
365
|
+
const pathArgs = args;
|
|
366
|
+
const docRef = documentRef(...pathArgs);
|
|
367
|
+
batch.update(docRef, data);
|
|
368
|
+
},
|
|
369
|
+
delete: (...args) => {
|
|
370
|
+
const docRef = documentRef(...args);
|
|
371
|
+
batch.delete(docRef);
|
|
372
|
+
},
|
|
373
|
+
commit: async () => {
|
|
374
|
+
await batch.commit();
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/methods/bulk.ts
|
|
382
|
+
function createBulkMethods(db) {
|
|
383
|
+
return {
|
|
384
|
+
// Set multiple documents with automatic batching (500 ops per flush)
|
|
385
|
+
set: async (items) => {
|
|
386
|
+
const bulkWriter = db.bulkWriter();
|
|
387
|
+
let pendingOps = 0;
|
|
388
|
+
for (const item of items) {
|
|
389
|
+
if (!item) continue;
|
|
390
|
+
const { docRef, data, merge = true } = item;
|
|
391
|
+
bulkWriter.set(docRef, data, { merge });
|
|
392
|
+
pendingOps++;
|
|
393
|
+
if (pendingOps >= 500) {
|
|
394
|
+
await bulkWriter.flush();
|
|
395
|
+
pendingOps = 0;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
await bulkWriter.close();
|
|
399
|
+
},
|
|
400
|
+
// Update multiple documents with automatic batching
|
|
401
|
+
update: async (items) => {
|
|
402
|
+
const bulkWriter = db.bulkWriter();
|
|
403
|
+
let pendingOps = 0;
|
|
404
|
+
for (const item of items) {
|
|
405
|
+
if (!item) continue;
|
|
406
|
+
const { docRef, data } = item;
|
|
407
|
+
bulkWriter.update(docRef, data);
|
|
408
|
+
pendingOps++;
|
|
409
|
+
if (pendingOps >= 500) {
|
|
410
|
+
await bulkWriter.flush();
|
|
411
|
+
pendingOps = 0;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
await bulkWriter.close();
|
|
415
|
+
},
|
|
416
|
+
// Delete multiple documents with automatic batching
|
|
417
|
+
delete: async (docRefs) => {
|
|
418
|
+
const bulkWriter = db.bulkWriter();
|
|
419
|
+
let pendingOps = 0;
|
|
420
|
+
for (const docRef of docRefs) {
|
|
421
|
+
if (!docRef) continue;
|
|
422
|
+
bulkWriter.delete(docRef);
|
|
423
|
+
pendingOps++;
|
|
424
|
+
if (pendingOps >= 500) {
|
|
425
|
+
await bulkWriter.flush();
|
|
426
|
+
pendingOps = 0;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
await bulkWriter.close();
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/methods/crud.ts
|
|
435
|
+
function createCrudMethods(actualCollection, documentRef) {
|
|
436
|
+
const create = async (data) => {
|
|
437
|
+
if (!actualCollection) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
"Cannot use create() on collection groups. Use set() with a specific document ID instead."
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const docRef = await actualCollection.add(data);
|
|
443
|
+
const createdDoc = await docRef.get();
|
|
444
|
+
return { ...createdDoc.data(), docId: docRef.id };
|
|
445
|
+
};
|
|
446
|
+
const set = async (...args) => {
|
|
447
|
+
const lastArg = args[args.length - 1];
|
|
448
|
+
const hasOptions = typeof lastArg === "object" && lastArg !== null && "merge" in lastArg;
|
|
449
|
+
const data = hasOptions ? args[args.length - 2] : args[args.length - 1];
|
|
450
|
+
const pathArgs = hasOptions ? args.slice(0, -2) : args.slice(0, -1);
|
|
451
|
+
const mergeOption = hasOptions ? lastArg : { merge: true };
|
|
452
|
+
const docRef = documentRef(...pathArgs);
|
|
453
|
+
await docRef.set(data, mergeOption);
|
|
454
|
+
const setDocument = await docRef.get();
|
|
455
|
+
return { ...setDocument.data(), docId: docRef.id };
|
|
456
|
+
};
|
|
457
|
+
const update = async (...args) => {
|
|
458
|
+
const data = args.pop();
|
|
459
|
+
const pathArgs = args;
|
|
460
|
+
const docRef = documentRef(...pathArgs);
|
|
461
|
+
await docRef.update(data);
|
|
462
|
+
const updatedDoc = await docRef.get();
|
|
463
|
+
return { ...updatedDoc.data(), docId: docRef.id };
|
|
464
|
+
};
|
|
465
|
+
const deleteMethod = async (...args) => {
|
|
466
|
+
const docRef = documentRef(...args);
|
|
467
|
+
await docRef.delete();
|
|
468
|
+
};
|
|
469
|
+
return {
|
|
470
|
+
create,
|
|
471
|
+
set,
|
|
472
|
+
update,
|
|
473
|
+
delete: deleteMethod
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/methods/get.ts
|
|
478
|
+
function createGetMethods(collectionRef, foreignKeys, actualCollection, documentRef) {
|
|
479
|
+
const getMethods = {};
|
|
480
|
+
getMethods.byList = async (key, values, operator = "in", returnDoc = false) => {
|
|
481
|
+
if (values.length === 0) return [];
|
|
482
|
+
const results = [];
|
|
483
|
+
const chunks = chunkArray2(values, 30);
|
|
484
|
+
for (const chunk of chunks) {
|
|
485
|
+
let q = collectionRef;
|
|
486
|
+
q = q.where(key, operator, chunk);
|
|
487
|
+
const snapshot = await q.get();
|
|
488
|
+
snapshot.forEach((doc) => {
|
|
489
|
+
const data = doc.data();
|
|
490
|
+
results.push(returnDoc ? { data, doc } : { ...data, docId: doc.id });
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return results;
|
|
494
|
+
};
|
|
495
|
+
foreignKeys.forEach((foreignKey) => {
|
|
496
|
+
const methodName = `by${capitalize(String(foreignKey))}`;
|
|
497
|
+
getMethods[methodName] = async (value, returnDoc = false) => {
|
|
498
|
+
if (String(foreignKey) === "docId") {
|
|
499
|
+
const docRef = documentRef(value);
|
|
500
|
+
const doc2 = await docRef.get();
|
|
501
|
+
if (!doc2.exists) return null;
|
|
502
|
+
const data2 = doc2.data();
|
|
503
|
+
return returnDoc ? { data: data2, doc: doc2 } : { ...data2, docId: doc2.id };
|
|
504
|
+
}
|
|
505
|
+
let q = collectionRef;
|
|
506
|
+
q = q.where(String(foreignKey), "==", value).limit(1);
|
|
507
|
+
const snapshot = await q.get();
|
|
508
|
+
if (snapshot.empty) return null;
|
|
509
|
+
const doc = snapshot.docs[0];
|
|
510
|
+
if (!doc) return null;
|
|
511
|
+
const data = doc.data();
|
|
512
|
+
return returnDoc ? { data, doc } : { ...data, docId: doc.id };
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
return getMethods;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/methods/relations.ts
|
|
519
|
+
function createPopulateMethods(config, allRepositories) {
|
|
520
|
+
return {
|
|
521
|
+
populate: async (document, relationKey) => {
|
|
522
|
+
if (!config.relationalKeys) {
|
|
523
|
+
return { ...document, populated: {} };
|
|
524
|
+
}
|
|
525
|
+
const keys = Array.isArray(relationKey) ? relationKey : [relationKey];
|
|
526
|
+
const result = { ...document };
|
|
527
|
+
const populated = {};
|
|
528
|
+
for (const key of keys) {
|
|
529
|
+
const relation = config.relationalKeys?.[key];
|
|
530
|
+
if (!relation) {
|
|
531
|
+
console.warn(
|
|
532
|
+
`[populate] Relation "${String(key)}" not found in config`
|
|
533
|
+
);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const targetRepo = allRepositories[relation.repo];
|
|
537
|
+
if (!targetRepo) {
|
|
538
|
+
console.warn(
|
|
539
|
+
`[populate] Repository "${relation.repo}" not found in mapping`
|
|
540
|
+
);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const fieldValue = document[key];
|
|
544
|
+
if (fieldValue === void 0 || fieldValue === null) {
|
|
545
|
+
populated[relation.repo] = relation.type === "one" ? null : [];
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
if (relation.type === "one") {
|
|
550
|
+
const getMethod = `by${capitalize2(relation.key)}`;
|
|
551
|
+
if (typeof targetRepo.get?.[getMethod] === "function") {
|
|
552
|
+
populated[relation.repo] = await targetRepo.get[getMethod](
|
|
553
|
+
fieldValue
|
|
554
|
+
);
|
|
555
|
+
} else {
|
|
556
|
+
console.warn(
|
|
557
|
+
`[populate] Method "get.${getMethod}" not found in ${relation.repo}`
|
|
558
|
+
);
|
|
559
|
+
populated[relation.repo] = null;
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
const queryMethod = `by${capitalize2(relation.key)}`;
|
|
563
|
+
if (typeof targetRepo.query[queryMethod] === "function") {
|
|
564
|
+
populated[relation.repo] = await targetRepo.query[queryMethod](
|
|
565
|
+
fieldValue
|
|
566
|
+
);
|
|
567
|
+
} else {
|
|
568
|
+
console.warn(
|
|
569
|
+
`[populate] Method "query.${queryMethod}" not found in ${relation.repo}`
|
|
570
|
+
);
|
|
571
|
+
populated[relation.repo] = [];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.error(`[populate] Error populating "${String(key)}":`, error);
|
|
576
|
+
populated[relation.repo] = relation.type === "one" ? null : [];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { ...result, populated };
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function capitalize2(str) {
|
|
584
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/methods/transaction.ts
|
|
588
|
+
function createTransactionMethods(db, documentRef) {
|
|
589
|
+
return {
|
|
590
|
+
run: async (updateFunction) => {
|
|
591
|
+
return db.runTransaction(async (rawTransaction) => {
|
|
592
|
+
const typedTransaction = {
|
|
593
|
+
// Type-safe get method
|
|
594
|
+
get: async (...args) => {
|
|
595
|
+
const docRef = documentRef(...args);
|
|
596
|
+
const docSnap = await rawTransaction.get(docRef);
|
|
597
|
+
if (!docSnap.exists) return null;
|
|
598
|
+
return { ...docSnap.data(), docId: docSnap.id };
|
|
599
|
+
},
|
|
600
|
+
// Type-safe set method
|
|
601
|
+
set: (...args) => {
|
|
602
|
+
const options = args[args.length - 1];
|
|
603
|
+
const hasOptions = typeof options === "object" && options !== null && "merge" in options;
|
|
604
|
+
const data = hasOptions ? args[args.length - 2] : args[args.length - 1];
|
|
605
|
+
const pathArgs = hasOptions ? args.slice(0, -2) : args.slice(0, -1);
|
|
606
|
+
const mergeOption = hasOptions ? options : { merge: true };
|
|
607
|
+
const docRef = documentRef(...pathArgs);
|
|
608
|
+
rawTransaction.set(docRef, data, mergeOption);
|
|
609
|
+
},
|
|
610
|
+
// Type-safe update method
|
|
611
|
+
update: (...args) => {
|
|
612
|
+
const data = args[args.length - 1];
|
|
613
|
+
const pathArgs = args.slice(0, -1);
|
|
614
|
+
const docRef = documentRef(...pathArgs);
|
|
615
|
+
rawTransaction.update(docRef, data);
|
|
616
|
+
},
|
|
617
|
+
// Delete method
|
|
618
|
+
delete: (...args) => {
|
|
619
|
+
const docRef = documentRef(...args);
|
|
620
|
+
rawTransaction.delete(docRef);
|
|
621
|
+
},
|
|
622
|
+
// Access to raw transaction
|
|
623
|
+
raw: rawTransaction
|
|
624
|
+
};
|
|
625
|
+
return updateFunction(typedTransaction);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/repositories/factory.ts
|
|
632
|
+
function createRepository(db, config, allRepositories = {}) {
|
|
633
|
+
const collectionRef = config.isGroup ? db.collectionGroup(config.path) : db.collection(config.path);
|
|
634
|
+
const actualCollection = config.isGroup ? null : db.collection(config.path);
|
|
635
|
+
const documentRef = (...args) => config.refCb(db, ...args);
|
|
636
|
+
const getMethods = createGetMethods(
|
|
637
|
+
collectionRef,
|
|
638
|
+
config.foreignKeys,
|
|
639
|
+
actualCollection,
|
|
640
|
+
documentRef
|
|
641
|
+
);
|
|
642
|
+
const queryMethods = createQueryMethods(
|
|
643
|
+
collectionRef,
|
|
644
|
+
config.queryKeys
|
|
645
|
+
);
|
|
646
|
+
const aggregateMethods = createAggregateMethods(collectionRef);
|
|
647
|
+
const crudMethods = createCrudMethods(actualCollection, documentRef);
|
|
648
|
+
const batchMethods = createBatchMethods(db, documentRef);
|
|
649
|
+
const transactionMethods = createTransactionMethods(db, documentRef);
|
|
650
|
+
const bulkMethods = createBulkMethods(db);
|
|
651
|
+
const populateMethods = createPopulateMethods(config, allRepositories);
|
|
652
|
+
return {
|
|
653
|
+
ref: collectionRef,
|
|
654
|
+
documentRef,
|
|
655
|
+
get: getMethods,
|
|
656
|
+
query: queryMethods,
|
|
657
|
+
aggregate: aggregateMethods,
|
|
658
|
+
...crudMethods,
|
|
659
|
+
batch: batchMethods,
|
|
660
|
+
transaction: transactionMethods,
|
|
661
|
+
bulk: bulkMethods,
|
|
662
|
+
...populateMethods
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// index.ts
|
|
667
|
+
function createRepositoryConfig() {
|
|
668
|
+
return (config) => {
|
|
669
|
+
return {
|
|
670
|
+
...config,
|
|
671
|
+
type: null,
|
|
672
|
+
documentRef: null,
|
|
673
|
+
update: null
|
|
674
|
+
};
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function buildRepositoryRelations(mapping, relations) {
|
|
678
|
+
const result = { ...mapping };
|
|
679
|
+
for (const repoKey in relations) {
|
|
680
|
+
if (relations[repoKey]) {
|
|
681
|
+
result[repoKey] = {
|
|
682
|
+
...mapping[repoKey],
|
|
683
|
+
relationalKeys: relations[repoKey]
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
var RepositoryMapping = class {
|
|
690
|
+
/**
|
|
691
|
+
* Creates a new RepositoryMapping instance
|
|
692
|
+
* @param db - Firestore instance from firebase-admin
|
|
693
|
+
* @param mapping - Repository configuration mapping
|
|
694
|
+
*/
|
|
695
|
+
constructor(db, mapping) {
|
|
696
|
+
this.repositoryCache = /* @__PURE__ */ new Map();
|
|
697
|
+
this.allRepositories = {};
|
|
698
|
+
this.db = db;
|
|
699
|
+
this.mapping = mapping;
|
|
700
|
+
this.initializeRepositories();
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Initialize all repositories in two passes to handle circular dependencies
|
|
704
|
+
* @private
|
|
705
|
+
*/
|
|
706
|
+
initializeRepositories() {
|
|
707
|
+
for (const key of Object.keys(this.mapping)) {
|
|
708
|
+
this.allRepositories[key] = createRepository(
|
|
709
|
+
this.db,
|
|
710
|
+
this.mapping[key],
|
|
711
|
+
{}
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
for (const key of Object.keys(this.mapping)) {
|
|
715
|
+
this.allRepositories[key] = createRepository(
|
|
716
|
+
this.db,
|
|
717
|
+
this.mapping[key],
|
|
718
|
+
this.allRepositories
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Gets a repository (already initialized)
|
|
724
|
+
* @template K - Repository key
|
|
725
|
+
* @param key - Repository identifier
|
|
726
|
+
* @returns Configured repository instance
|
|
727
|
+
*/
|
|
728
|
+
getRepository(key) {
|
|
729
|
+
return this.allRepositories[key];
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
function createRepositoryMapping(db, mapping) {
|
|
733
|
+
const instance = new RepositoryMapping(db, mapping);
|
|
734
|
+
return new Proxy(instance, {
|
|
735
|
+
get(target, prop) {
|
|
736
|
+
if (typeof prop === "string" && prop in mapping) {
|
|
737
|
+
return target.getRepository(prop);
|
|
738
|
+
}
|
|
739
|
+
return target[prop];
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export { RepositoryMapping, applyQueryOptions as applyPaginationQueryOptions, buildAndExecuteQuery, buildRepositoryRelations, createPaginationIterator, createRepositoryConfig, createRepositoryMapping, executePaginatedQuery };
|
|
745
|
+
//# sourceMappingURL=index.js.map
|
|
746
|
+
//# sourceMappingURL=index.js.map
|