@webda/gcp 4.0.0-beta.2 → 4.0.0-beta.4
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 +54 -4
- package/lib/index.d.ts +4 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -4
- package/lib/index.js.map +1 -1
- package/lib/services/firestore.service.d.ts +237 -0
- package/lib/services/firestore.service.d.ts.map +1 -0
- package/lib/services/firestore.service.js +693 -0
- package/lib/services/firestore.service.js.map +1 -0
- package/lib/services/index.d.ts +1 -1
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +1 -1
- package/lib/services/index.js.map +1 -1
- package/lib/services/{kms.d.ts → kms.service.d.ts} +14 -15
- package/lib/services/kms.service.d.ts.map +1 -0
- package/lib/services/{kms.js → kms.service.js} +18 -16
- package/lib/services/kms.service.js.map +1 -0
- package/lib/services/{pubsub.d.ts → pubsub.service.d.ts} +23 -4
- package/lib/services/pubsub.service.d.ts.map +1 -0
- package/lib/services/{pubsub.js → pubsub.service.js} +36 -10
- package/lib/services/pubsub.service.js.map +1 -0
- package/lib/services/{queue.d.ts → queue.service.d.ts} +32 -16
- package/lib/services/queue.service.d.ts.map +1 -0
- package/lib/services/{queue.js → queue.service.js} +63 -32
- package/lib/services/queue.service.js.map +1 -0
- package/lib/services/storage.service.d.ts +268 -0
- package/lib/services/storage.service.d.ts.map +1 -0
- package/lib/services/{storage.js → storage.service.js} +116 -69
- package/lib/services/storage.service.js.map +1 -0
- package/package.json +33 -43
- package/webda.module.json +1122 -1440
- package/lib/index.spec.d.ts +0 -5
- package/lib/index.spec.d.ts.map +0 -1
- package/lib/index.spec.js +0 -7
- package/lib/index.spec.js.map +0 -1
- package/lib/services/firestore.d.ts +0 -135
- package/lib/services/firestore.d.ts.map +0 -1
- package/lib/services/firestore.js +0 -423
- package/lib/services/firestore.js.map +0 -1
- package/lib/services/firestore.spec.d.ts +0 -2
- package/lib/services/firestore.spec.d.ts.map +0 -1
- package/lib/services/firestore.spec.js +0 -187
- package/lib/services/firestore.spec.js.map +0 -1
- package/lib/services/kms.d.ts.map +0 -1
- package/lib/services/kms.js.map +0 -1
- package/lib/services/kms.spec.d.ts +0 -2
- package/lib/services/kms.spec.d.ts.map +0 -1
- package/lib/services/kms.spec.js +0 -53
- package/lib/services/kms.spec.js.map +0 -1
- package/lib/services/pubsub.d.ts.map +0 -1
- package/lib/services/pubsub.js.map +0 -1
- package/lib/services/pubsub.spec.d.ts +0 -2
- package/lib/services/pubsub.spec.d.ts.map +0 -1
- package/lib/services/pubsub.spec.js +0 -95
- package/lib/services/pubsub.spec.js.map +0 -1
- package/lib/services/queue.d.ts.map +0 -1
- package/lib/services/queue.js.map +0 -1
- package/lib/services/queue.spec.d.ts +0 -2
- package/lib/services/queue.spec.d.ts.map +0 -1
- package/lib/services/queue.spec.js +0 -117
- package/lib/services/queue.spec.js.map +0 -1
- package/lib/services/storage.d.ts +0 -215
- package/lib/services/storage.d.ts.map +0 -1
- package/lib/services/storage.js.map +0 -1
- package/lib/services/storage.spec.d.ts +0 -2
- package/lib/services/storage.spec.d.ts.map +0 -1
- package/lib/services/storage.spec.js +0 -224
- package/lib/services/storage.spec.js.map +0 -1
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { FieldValue, Firestore, Timestamp } from "@google-cloud/firestore";
|
|
36
|
+
import { EventRepository, InstanceCache, MemoryRepository, Store, StoreNotFoundError, StoreParameters, UpdateConditionFailError, useModel, useModelMetadata } from "@webda/core";
|
|
37
|
+
import * as WebdaQL from "@webda/ql";
|
|
38
|
+
/**
|
|
39
|
+
* Firestore error code for a missing document
|
|
40
|
+
*/
|
|
41
|
+
const NOT_FOUND = 5;
|
|
42
|
+
/**
|
|
43
|
+
* Firestore error code for an already existing document
|
|
44
|
+
*/
|
|
45
|
+
const ALREADY_EXISTS = 6;
|
|
46
|
+
/**
|
|
47
|
+
* Firebase parameters
|
|
48
|
+
*/
|
|
49
|
+
export class FireStoreParameters extends StoreParameters {
|
|
50
|
+
/**
|
|
51
|
+
* @override
|
|
52
|
+
* @param params - raw parameters
|
|
53
|
+
* @returns this
|
|
54
|
+
*/
|
|
55
|
+
load(params = {}) {
|
|
56
|
+
super.load(params);
|
|
57
|
+
this.compoundIndexes ?? (this.compoundIndexes = []);
|
|
58
|
+
this.collections ?? (this.collections = {});
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Recursively convert a value to what Firestore can store
|
|
64
|
+
*
|
|
65
|
+
* Dates are kept as is (Firestore stores them as Timestamp), objects exposing a
|
|
66
|
+
* `toJSON` (models, binaries) are serialized first, functions and undefined are dropped
|
|
67
|
+
*
|
|
68
|
+
* @param value - the value to convert
|
|
69
|
+
* @returns the Firestore compatible value
|
|
70
|
+
*/
|
|
71
|
+
export function toFirestore(value) {
|
|
72
|
+
if (value === undefined || value === null || value instanceof Date) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
return value.map(v => toFirestore(v));
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "object") {
|
|
79
|
+
if (typeof value.toJSON === "function") {
|
|
80
|
+
const json = value.toJSON();
|
|
81
|
+
if (json !== value) {
|
|
82
|
+
return toFirestore(json);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const res = {};
|
|
86
|
+
for (const key of Object.keys(value)) {
|
|
87
|
+
if (value[key] === undefined || typeof value[key] === "function") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
res[key] = toFirestore(value[key]);
|
|
91
|
+
}
|
|
92
|
+
return res;
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Recursively replace Timestamp by Date
|
|
98
|
+
* @param doc - the Firestore document data
|
|
99
|
+
* @returns the data with Date objects
|
|
100
|
+
*/
|
|
101
|
+
export function giveDatesBack(doc) {
|
|
102
|
+
if (doc instanceof Timestamp) {
|
|
103
|
+
return doc.toDate();
|
|
104
|
+
}
|
|
105
|
+
else if (Array.isArray(doc)) {
|
|
106
|
+
return doc.map(d => giveDatesBack(d));
|
|
107
|
+
}
|
|
108
|
+
else if (doc instanceof Object) {
|
|
109
|
+
const res = {};
|
|
110
|
+
Object.keys(doc).forEach(k => {
|
|
111
|
+
res[k] = giveDatesBack(doc[k]);
|
|
112
|
+
});
|
|
113
|
+
return res;
|
|
114
|
+
}
|
|
115
|
+
return doc;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Firestore-backed repository for a single model class
|
|
119
|
+
*
|
|
120
|
+
* Every object is stored as a document of the collection, keyed by its primary key.
|
|
121
|
+
* The inherited MemoryRepository is only used for its helpers, every storage
|
|
122
|
+
* method hits Firestore.
|
|
123
|
+
*/
|
|
124
|
+
export class FireStoreRepository extends MemoryRepository {
|
|
125
|
+
/**
|
|
126
|
+
* @param model - the model class
|
|
127
|
+
* @param pks - primary key field names
|
|
128
|
+
* @param firestore - the Firestore client
|
|
129
|
+
* @param collection - the collection name
|
|
130
|
+
* @param indexes - compound indexes declared on the store
|
|
131
|
+
* @param log - logger of the store
|
|
132
|
+
* @param separator - primary key separator
|
|
133
|
+
*/
|
|
134
|
+
constructor(model, pks, firestore, collection, indexes = [], log = () => { }, separator) {
|
|
135
|
+
// Pass an empty Map — we do NOT use in-memory storage
|
|
136
|
+
super(model, pks, separator, new Map());
|
|
137
|
+
this.firestore = firestore;
|
|
138
|
+
this.collection = collection;
|
|
139
|
+
this.log = log;
|
|
140
|
+
/**
|
|
141
|
+
* Compound indexes available, keyed by the sorted fields joined with "/"
|
|
142
|
+
*/
|
|
143
|
+
this.compoundIndexes = {};
|
|
144
|
+
indexes.forEach(a => {
|
|
145
|
+
const key = Object.keys(a).sort().join("/");
|
|
146
|
+
// Should contain the array of accessible order
|
|
147
|
+
this.compoundIndexes[key] = {};
|
|
148
|
+
Object.keys(a).forEach(field => {
|
|
149
|
+
var _a;
|
|
150
|
+
(_a = this.compoundIndexes[key])[field] ?? (_a[field] = new Set());
|
|
151
|
+
this.compoundIndexes[key][field].add(a[field]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The backing collection name for this repository's model
|
|
157
|
+
* @returns the collection name
|
|
158
|
+
*/
|
|
159
|
+
getCollection() {
|
|
160
|
+
return this.collection;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get Document Reference
|
|
164
|
+
* @param primaryKey - the object or primary key
|
|
165
|
+
* @returns the document reference
|
|
166
|
+
*/
|
|
167
|
+
getDocumentRef(primaryKey) {
|
|
168
|
+
return this.firestore.doc(`${this.collection}/${this.getPrimaryKey(primaryKey).toString()}`);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Deserialize a Firestore document into a model instance
|
|
172
|
+
* @param data - the document data
|
|
173
|
+
* @returns the model instance
|
|
174
|
+
*/
|
|
175
|
+
fromFirestore(data) {
|
|
176
|
+
data = giveDatesBack(data);
|
|
177
|
+
const instance = new this.model({});
|
|
178
|
+
if (typeof instance.load === "function") {
|
|
179
|
+
instance.load(data);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
Object.assign(instance, data);
|
|
183
|
+
}
|
|
184
|
+
return instance;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Verify a write condition
|
|
188
|
+
* @param uid - the object key, for the error
|
|
189
|
+
* @param data - the current data
|
|
190
|
+
* @param field - the field to check
|
|
191
|
+
* @param condition - the expected value
|
|
192
|
+
*/
|
|
193
|
+
checkFirestoreCondition(uid, data, field, condition) {
|
|
194
|
+
if (!field) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const current = data?.[field] instanceof Timestamp ? data[field].toDate().toISOString() : data?.[field];
|
|
198
|
+
if (condition instanceof Date) {
|
|
199
|
+
condition = condition.toISOString();
|
|
200
|
+
}
|
|
201
|
+
if (current !== condition) {
|
|
202
|
+
throw new UpdateConditionFailError(uid, field, condition);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Translate Firestore not found error
|
|
207
|
+
* @param uid - the object key
|
|
208
|
+
* @param err - the error raised
|
|
209
|
+
*/
|
|
210
|
+
translateError(uid, err) {
|
|
211
|
+
if (err?.code === NOT_FOUND) {
|
|
212
|
+
throw new StoreNotFoundError(uid, this.collection);
|
|
213
|
+
}
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
/** @override */
|
|
217
|
+
async get(primaryKey) {
|
|
218
|
+
const ref = this.getDocumentRef(primaryKey);
|
|
219
|
+
const doc = (await ref.get()).data();
|
|
220
|
+
if (!doc) {
|
|
221
|
+
throw new StoreNotFoundError(ref.id, this.collection);
|
|
222
|
+
}
|
|
223
|
+
return this.fromFirestore(doc);
|
|
224
|
+
}
|
|
225
|
+
/** @override */
|
|
226
|
+
async exists(primaryKey) {
|
|
227
|
+
return (await this.getDocumentRef(primaryKey).get()).exists;
|
|
228
|
+
}
|
|
229
|
+
/** @override */
|
|
230
|
+
async create(data, save = true) {
|
|
231
|
+
// Build the item first: a primary key generated by the model is the document key
|
|
232
|
+
const item = this.buildItem(data);
|
|
233
|
+
if (save !== false) {
|
|
234
|
+
try {
|
|
235
|
+
await this.getDocumentRef(item).create(toFirestore(item));
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
if (err?.code === ALREADY_EXISTS) {
|
|
239
|
+
throw new Error(`Already exists: ${this.getPrimaryKey(item).toString()}`);
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return item;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Implement both update and patch
|
|
248
|
+
* @param merge - merge with the existing document (patch) or replace it (update)
|
|
249
|
+
* @param primaryKey - the object or primary key
|
|
250
|
+
* @param data - the data to write
|
|
251
|
+
* @param conditionField - the field to check
|
|
252
|
+
* @param condition - the expected value
|
|
253
|
+
*/
|
|
254
|
+
async setDocument(merge, primaryKey, data, conditionField, condition) {
|
|
255
|
+
const update = toFirestore(data);
|
|
256
|
+
const docRef = this.getDocumentRef(primaryKey);
|
|
257
|
+
await this.firestore.runTransaction(async (t) => {
|
|
258
|
+
const doc = await t.get(docRef);
|
|
259
|
+
if (!doc.exists) {
|
|
260
|
+
throw new StoreNotFoundError(docRef.id, this.collection);
|
|
261
|
+
}
|
|
262
|
+
this.checkFirestoreCondition(docRef.id, doc.data(), conditionField, condition);
|
|
263
|
+
t.set(docRef, update, { merge });
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** @override */
|
|
267
|
+
async update(data, conditionField, condition) {
|
|
268
|
+
await this.setDocument(false, data, data, conditionField, condition);
|
|
269
|
+
}
|
|
270
|
+
/** @override */
|
|
271
|
+
async patch(primaryKey, data, conditionField, condition) {
|
|
272
|
+
await this.setDocument(true, primaryKey, data, conditionField, condition);
|
|
273
|
+
}
|
|
274
|
+
/** @override */
|
|
275
|
+
async delete(primaryKey, conditionField, condition) {
|
|
276
|
+
const docRef = this.getDocumentRef(primaryKey);
|
|
277
|
+
await this.firestore.runTransaction(async (t) => {
|
|
278
|
+
const doc = await t.get(docRef);
|
|
279
|
+
if (conditionField) {
|
|
280
|
+
this.checkFirestoreCondition(docRef.id, doc.data(), conditionField, condition);
|
|
281
|
+
}
|
|
282
|
+
t.delete(docRef);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/** @override */
|
|
286
|
+
async removeAttribute(primaryKey, attribute, conditionField, condition) {
|
|
287
|
+
const docRef = this.getDocumentRef(primaryKey);
|
|
288
|
+
await this.firestore.runTransaction(async (t) => {
|
|
289
|
+
const doc = await t.get(docRef);
|
|
290
|
+
if (!doc.exists) {
|
|
291
|
+
throw new StoreNotFoundError(docRef.id, this.collection);
|
|
292
|
+
}
|
|
293
|
+
this.checkFirestoreCondition(docRef.id, doc.data(), conditionField, condition);
|
|
294
|
+
t.update(docRef, {
|
|
295
|
+
[attribute]: FieldValue.delete()
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** @override */
|
|
300
|
+
async incrementAttributes(primaryKey, info, _conditionField, _condition) {
|
|
301
|
+
const docRef = this.getDocumentRef(primaryKey);
|
|
302
|
+
const entries = Array.isArray(info)
|
|
303
|
+
? info.map((e) => typeof e === "string" ? { property: e, value: 1 } : { property: e.property, value: e.value ?? 1 })
|
|
304
|
+
: Object.entries(info).map(([property, value]) => ({ property, value: value }));
|
|
305
|
+
const args = {
|
|
306
|
+
_lastUpdate: new Date()
|
|
307
|
+
};
|
|
308
|
+
entries.forEach(p => {
|
|
309
|
+
args[p.property] = FieldValue.increment(p.value);
|
|
310
|
+
});
|
|
311
|
+
try {
|
|
312
|
+
await docRef.update(args);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
this.translateError(docRef.id, err);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** @override */
|
|
319
|
+
async upsertItemToCollection(primaryKey, collection, item, index, itemWriteConditionField, itemWriteCondition) {
|
|
320
|
+
const docRef = this.getDocumentRef(primaryKey);
|
|
321
|
+
const prop = String(collection);
|
|
322
|
+
if (index !== undefined) {
|
|
323
|
+
await this.firestore.runTransaction(async (t) => {
|
|
324
|
+
const doc = await t.get(docRef);
|
|
325
|
+
if (!doc.exists) {
|
|
326
|
+
throw new StoreNotFoundError(docRef.id, this.collection);
|
|
327
|
+
}
|
|
328
|
+
const data = doc.data();
|
|
329
|
+
data[prop] ?? (data[prop] = []);
|
|
330
|
+
if (itemWriteCondition !== undefined) {
|
|
331
|
+
this.checkFirestoreCondition(docRef.id, data[prop][index], itemWriteConditionField, itemWriteCondition);
|
|
332
|
+
}
|
|
333
|
+
data[prop][index] = toFirestore(item);
|
|
334
|
+
t.update(docRef, {
|
|
335
|
+
[prop]: data[prop],
|
|
336
|
+
_lastUpdate: new Date()
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
try {
|
|
342
|
+
await docRef.update({
|
|
343
|
+
[prop]: FieldValue.arrayUnion(toFirestore(item)),
|
|
344
|
+
_lastUpdate: new Date()
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
this.translateError(docRef.id, err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** @override */
|
|
353
|
+
async deleteItemFromCollection(primaryKey, collection, index, itemWriteConditionField, itemWriteCondition) {
|
|
354
|
+
const docRef = this.getDocumentRef(primaryKey);
|
|
355
|
+
const prop = String(collection);
|
|
356
|
+
await this.firestore.runTransaction(async (t) => {
|
|
357
|
+
const doc = await t.get(docRef);
|
|
358
|
+
if (!doc.exists) {
|
|
359
|
+
throw new StoreNotFoundError(docRef.id, this.collection);
|
|
360
|
+
}
|
|
361
|
+
const data = doc.data();
|
|
362
|
+
const items = Array.isArray(data[prop]) ? data[prop] : [];
|
|
363
|
+
if (index < 0 || index >= items.length) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (itemWriteCondition !== undefined) {
|
|
367
|
+
this.checkFirestoreCondition(docRef.id, items[index], itemWriteConditionField, itemWriteCondition);
|
|
368
|
+
}
|
|
369
|
+
items.splice(index, 1);
|
|
370
|
+
t.update(docRef, {
|
|
371
|
+
[prop]: items,
|
|
372
|
+
_lastUpdate: new Date()
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Run a parsed query against Firestore
|
|
378
|
+
*
|
|
379
|
+
* Firestore has strong limitations on what can be queried natively, the part of the
|
|
380
|
+
* query that cannot be translated is applied in memory and returned as `filter`
|
|
381
|
+
*
|
|
382
|
+
* @param parsedQuery - the parsed WebdaQL query
|
|
383
|
+
* @returns the results, the in-memory filter and the continuation token
|
|
384
|
+
*/
|
|
385
|
+
async find(parsedQuery) {
|
|
386
|
+
const offset = parseInt(parsedQuery.continuationToken || "0");
|
|
387
|
+
const limit = parsedQuery.limit || 1000;
|
|
388
|
+
let query = this.firestore.collection(this.collection);
|
|
389
|
+
if (offset) {
|
|
390
|
+
query = query.offset(offset);
|
|
391
|
+
}
|
|
392
|
+
const filter = new WebdaQL.AndExpression([]);
|
|
393
|
+
let rangeAttribute;
|
|
394
|
+
let toProcess;
|
|
395
|
+
if (!(parsedQuery.filter instanceof WebdaQL.AndExpression)) {
|
|
396
|
+
toProcess = new WebdaQL.AndExpression([parsedQuery.filter]);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
toProcess = parsedQuery.filter;
|
|
400
|
+
}
|
|
401
|
+
// A FALSE conjunct matches nothing: do not hit Firestore (never fail open)
|
|
402
|
+
if (toProcess.children.some(c => c instanceof WebdaQL.BooleanExpression && !c.value)) {
|
|
403
|
+
return { results: [], filter: true, continuationToken: undefined };
|
|
404
|
+
}
|
|
405
|
+
// A TRUE conjunct is neutral
|
|
406
|
+
toProcess = new WebdaQL.AndExpression(toProcess.children.filter(c => !(c instanceof WebdaQL.BooleanExpression)));
|
|
407
|
+
const queryAttributes = new Set();
|
|
408
|
+
let hasIn = false;
|
|
409
|
+
let hasContains = false;
|
|
410
|
+
toProcess.children.forEach((child) => {
|
|
411
|
+
if (!(child instanceof WebdaQL.ComparisonExpression)) {
|
|
412
|
+
// Do not manage OR yet
|
|
413
|
+
filter.children.push(child);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (child.operator === "LIKE") {
|
|
417
|
+
this.log("WARN", "Firebase do not natively have 'LIKE'");
|
|
418
|
+
// LIKE is not managed by Firestore
|
|
419
|
+
filter.children.push(child);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (child.operator === "IS NULL") {
|
|
423
|
+
// Firestore `== null` does not match documents missing the field
|
|
424
|
+
filter.children.push(child);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
let operator;
|
|
428
|
+
const attribute = child.attribute.join(".");
|
|
429
|
+
queryAttributes.add(attribute);
|
|
430
|
+
// Translate operators
|
|
431
|
+
if (["=", "IN", "CONTAINS"].includes(child.operator)) {
|
|
432
|
+
// = is permitted on every fields
|
|
433
|
+
if (child.operator === "=") {
|
|
434
|
+
operator = "==";
|
|
435
|
+
}
|
|
436
|
+
else if (child.operator === "IN") {
|
|
437
|
+
operator = "in";
|
|
438
|
+
if (child.value.length > 10) {
|
|
439
|
+
this.log("WARN", "Firebase cannot have more than 10 values for 'in'");
|
|
440
|
+
filter.children.push(child);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (hasIn) {
|
|
444
|
+
this.log("WARN", "Firebase cannot have two 'in' (IN) clause");
|
|
445
|
+
filter.children.push(child);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
hasIn = true;
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
operator = "array-contains";
|
|
452
|
+
if (hasContains) {
|
|
453
|
+
this.log("WARN", "Firebase cannot have two 'array-contains' (CONTAINS) clause");
|
|
454
|
+
filter.children.push(child);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
hasContains = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// Requires compoundIndex
|
|
462
|
+
if (queryAttributes.size > 1 && !this.compoundIndexes[[...queryAttributes.values()].sort().join("/")]) {
|
|
463
|
+
this.log("WARN", "Compound index not defined");
|
|
464
|
+
filter.children.push(child);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// Firestore `!= null` matches documents where the field exists and is not null
|
|
468
|
+
const isNotNull = child.operator === "IS NOT NULL";
|
|
469
|
+
if (child.operator !== "!=" && !isNotNull) {
|
|
470
|
+
rangeAttribute ?? (rangeAttribute = attribute);
|
|
471
|
+
// Range need to apply on only one attribute
|
|
472
|
+
if (rangeAttribute !== attribute) {
|
|
473
|
+
filter.children.push(child);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
operator = isNotNull ? "!=" : child.operator;
|
|
478
|
+
}
|
|
479
|
+
query = query.where(attribute, operator, child.operator === "IS NOT NULL" ? null : child.value);
|
|
480
|
+
});
|
|
481
|
+
// OrderBy have quite some complexity with FireStore
|
|
482
|
+
query = this.handleOrderBy(query, parsedQuery.orderBy, rangeAttribute);
|
|
483
|
+
const res = await query.limit(limit).get();
|
|
484
|
+
let results = res.docs.map(d => this.fromFirestore(d.data()));
|
|
485
|
+
if (filter.children.length) {
|
|
486
|
+
results = results.filter(r => filter.eval(r));
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
results,
|
|
490
|
+
filter: filter.children.length ? filter : true,
|
|
491
|
+
continuationToken: res.docs.length >= limit ? (offset + limit).toString() : undefined
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Manage OrderBy complex condition on Firebase
|
|
496
|
+
* @param query - the Firestore query
|
|
497
|
+
* @param orderBy - the requested order
|
|
498
|
+
* @param rangeAttribute - the attribute used in a range condition if any
|
|
499
|
+
* @returns the Firestore query with ordering
|
|
500
|
+
*/
|
|
501
|
+
handleOrderBy(query, orderBy, rangeAttribute) {
|
|
502
|
+
// Manage order if index
|
|
503
|
+
if (!orderBy) {
|
|
504
|
+
return query;
|
|
505
|
+
}
|
|
506
|
+
const requiredIndex = orderBy.map(order => order.field).sort();
|
|
507
|
+
let orders;
|
|
508
|
+
// Require index with multiple ORDER BY
|
|
509
|
+
if (requiredIndex.length > 1) {
|
|
510
|
+
orders = this.compoundIndexes[requiredIndex.join("/")];
|
|
511
|
+
if (!orders) {
|
|
512
|
+
this.log("WARN", "Skip orderBy as we are missing the index");
|
|
513
|
+
return query;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Range must be the first orderBy
|
|
517
|
+
if (rangeAttribute) {
|
|
518
|
+
if (!orderBy.some(order => {
|
|
519
|
+
// Need to check the permitted orderBy and direction
|
|
520
|
+
if (order.field === rangeAttribute) {
|
|
521
|
+
query = query.orderBy(order.field, order.direction.toLowerCase());
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
})) {
|
|
526
|
+
this.log("WARN", "Skip orderBy as the range attribute is not within ORDER BY expression");
|
|
527
|
+
// If rangeAttribute is not in orderBy then skip the orderBy completely
|
|
528
|
+
return query;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Add remaining orderBy from index
|
|
532
|
+
orderBy
|
|
533
|
+
.filter(order => order.field !== rangeAttribute)
|
|
534
|
+
.forEach(order => {
|
|
535
|
+
if (!orders || orders[order.field].has(order.direction.toLowerCase())) {
|
|
536
|
+
query = query.orderBy(order.field, order.direction.toLowerCase());
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
this.log("WARN", "Skip orderBy as the direction does not match index");
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
return query;
|
|
543
|
+
}
|
|
544
|
+
/** @override */
|
|
545
|
+
async query(query) {
|
|
546
|
+
const parsed = typeof query === "string" ? WebdaQL.parse(query) : query;
|
|
547
|
+
const { results, continuationToken } = await this.find(parsed);
|
|
548
|
+
return { results, continuationToken };
|
|
549
|
+
}
|
|
550
|
+
/** @override */
|
|
551
|
+
async *iterate(query) {
|
|
552
|
+
const parsed = WebdaQL.parse(query);
|
|
553
|
+
parsed.limit ?? (parsed.limit = 100);
|
|
554
|
+
do {
|
|
555
|
+
const res = await this.find(parsed);
|
|
556
|
+
for (const item of res.results) {
|
|
557
|
+
yield item;
|
|
558
|
+
}
|
|
559
|
+
parsed.continuationToken = res.continuationToken;
|
|
560
|
+
} while (parsed.continuationToken);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Delete every document of the collection (used in tests)
|
|
564
|
+
*/
|
|
565
|
+
async clear() {
|
|
566
|
+
const refs = await this.firestore.collection(this.collection).listDocuments();
|
|
567
|
+
for (let i = 0; i < refs.length; i += 500) {
|
|
568
|
+
const batch = this.firestore.batch();
|
|
569
|
+
refs.slice(i, i + 500).forEach(ref => batch.delete(ref));
|
|
570
|
+
await batch.commit();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
let FireStore = (() => {
|
|
575
|
+
var _a;
|
|
576
|
+
let _classSuper = Store;
|
|
577
|
+
let _instanceExtraInitializers = [];
|
|
578
|
+
let _getRepository_decorators;
|
|
579
|
+
return _a = class FireStore extends _classSuper {
|
|
580
|
+
constructor() {
|
|
581
|
+
super(...arguments);
|
|
582
|
+
this.firestore = __runInitializers(this, _instanceExtraInitializers);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Create the Firestore client
|
|
586
|
+
* @override
|
|
587
|
+
*/
|
|
588
|
+
computeParameters() {
|
|
589
|
+
super.computeParameters();
|
|
590
|
+
this.firestore ?? (this.firestore = new Firestore({ ignoreUndefinedProperties: true }));
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Terminate the Firestore client
|
|
594
|
+
* @override
|
|
595
|
+
*/
|
|
596
|
+
async stop() {
|
|
597
|
+
await this.firestore?.terminate().catch(() => {
|
|
598
|
+
/* already terminated */
|
|
599
|
+
});
|
|
600
|
+
await super.stop();
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Resolve the collection name for a given model class
|
|
604
|
+
*
|
|
605
|
+
* Resolution order:
|
|
606
|
+
* 1. `parameters.collections[meta.Identifier]` — explicit per-model override
|
|
607
|
+
* 2. `parameters.collection` for the first model configured or when a single model is managed
|
|
608
|
+
* 3. Default — model identifier lowercased with "/" replaced by "_"
|
|
609
|
+
*
|
|
610
|
+
* @param model - the model class
|
|
611
|
+
* @returns the collection name
|
|
612
|
+
*/
|
|
613
|
+
resolveCollection(model) {
|
|
614
|
+
const meta = useModelMetadata(model);
|
|
615
|
+
if (!meta) {
|
|
616
|
+
return this.parameters.collection;
|
|
617
|
+
}
|
|
618
|
+
if (this.parameters.collections?.[meta.Identifier]) {
|
|
619
|
+
return this.parameters.collections[meta.Identifier];
|
|
620
|
+
}
|
|
621
|
+
if (this.parameters.collection &&
|
|
622
|
+
((this.parameters.models?.length ?? 0) <= 1 || this.parameters.models?.[0] === meta.Identifier)) {
|
|
623
|
+
return this.parameters.collection;
|
|
624
|
+
}
|
|
625
|
+
return meta.Identifier.toLowerCase().replace(/\//g, "_");
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Build and return a FireStoreRepository for the given model
|
|
629
|
+
*
|
|
630
|
+
* The result is cached per model class via `@InstanceCache`.
|
|
631
|
+
* @param model - the model class
|
|
632
|
+
* @returns a repository backed by Firestore
|
|
633
|
+
*/
|
|
634
|
+
getRepository(model) {
|
|
635
|
+
const meta = useModelMetadata(model);
|
|
636
|
+
this.firestore ?? (this.firestore = new Firestore({ ignoreUndefinedProperties: true }));
|
|
637
|
+
const inner = new FireStoreRepository(model, meta.PrimaryKey, this.firestore, this.resolveCollection(model), this.parameters.compoundIndexes, (level, ...args) => this.log(level, ...args), meta.PrimaryKeySeparator);
|
|
638
|
+
// Wrap in EventRepository so typed CRUD events fire; consumers reach them
|
|
639
|
+
// via useRepository(model).on(...).
|
|
640
|
+
return new EventRepository(model, meta.PrimaryKey, inner, meta.PrimaryKeySeparator);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Return the underlying FireStoreRepository for each model this store manages
|
|
644
|
+
* @returns the per-model FireStoreRepository instances
|
|
645
|
+
*/
|
|
646
|
+
getRepositories() {
|
|
647
|
+
const repos = [];
|
|
648
|
+
for (const modelId of Object.keys(this._modelsHierarchy ?? {})) {
|
|
649
|
+
let model;
|
|
650
|
+
try {
|
|
651
|
+
model = useModel(modelId);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (!model) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const repo = this.getRepository(model);
|
|
660
|
+
repos.push((repo.repository ?? repo));
|
|
661
|
+
}
|
|
662
|
+
return repos;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Delete every document of the managed collections (used in tests)
|
|
666
|
+
*/
|
|
667
|
+
async __clean() {
|
|
668
|
+
const done = new Set();
|
|
669
|
+
for (const repo of this.getRepositories()) {
|
|
670
|
+
if (done.has(repo.getCollection())) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
done.add(repo.getCollection());
|
|
674
|
+
await repo.clear();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
(() => {
|
|
679
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
680
|
+
_getRepository_decorators = [InstanceCache()];
|
|
681
|
+
__esDecorate(_a, null, _getRepository_decorators, { kind: "method", name: "getRepository", static: false, private: false, access: { has: obj => "getRepository" in obj, get: obj => obj.getRepository }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
682
|
+
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
683
|
+
})(),
|
|
684
|
+
_a;
|
|
685
|
+
})();
|
|
686
|
+
/**
|
|
687
|
+
* Implement Firebase abstraction within Webda
|
|
688
|
+
*
|
|
689
|
+
* @WebdaModda GoogleCloudFireStore
|
|
690
|
+
*/
|
|
691
|
+
export default FireStore;
|
|
692
|
+
export { FireStore };
|
|
693
|
+
//# sourceMappingURL=firestore.service.js.map
|