@flarequery/firebase 0.1.0 → 0.1.2
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 +125 -0
- package/dist/index.cjs +478 -236
- package/dist/index.d.cts +65 -31
- package/dist/index.d.ts +65 -31
- package/dist/index.js +476 -235
- package/package.json +6 -5
package/dist/index.cjs
CHANGED
|
@@ -32,160 +32,80 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
createFunction: () => createFunction,
|
|
34
34
|
createOnRequest: () => createOnRequest,
|
|
35
|
-
createServerlessApp: () => createServerlessApp
|
|
35
|
+
createServerlessApp: () => createServerlessApp,
|
|
36
|
+
extractContext: () => extractContext
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(index_exports);
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (input[i] === "(") {
|
|
70
|
-
tokens.push({ kind: "lparen", value: "(", position: i });
|
|
71
|
-
i++;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (input[i] === ")") {
|
|
75
|
-
tokens.push({ kind: "rparen", value: ")", position: i });
|
|
76
|
-
i++;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (input[i] === ":") {
|
|
80
|
-
tokens.push({ kind: "colon", value: ":", position: i });
|
|
81
|
-
i++;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (input[i] === '"') {
|
|
85
|
-
const start = i;
|
|
86
|
-
i++;
|
|
87
|
-
let str = "";
|
|
88
|
-
while (i < input.length && input[i] !== '"') {
|
|
89
|
-
str += input[i];
|
|
90
|
-
i++;
|
|
40
|
+
// src/adapter.ts
|
|
41
|
+
var FIRESTORE_GETALL_LIMIT = 300;
|
|
42
|
+
function createFirestoreAdapter(db) {
|
|
43
|
+
return {
|
|
44
|
+
async getOne(collection, id, fieldMask) {
|
|
45
|
+
const ref = db.collection(collection).doc(id);
|
|
46
|
+
const snapshot = await ref.get();
|
|
47
|
+
return adaptSnapshot(snapshot, fieldMask);
|
|
48
|
+
},
|
|
49
|
+
async getMany(collection, ids, fieldMask) {
|
|
50
|
+
if (ids.length === 0) return [];
|
|
51
|
+
const chunks = chunkArray(ids, FIRESTORE_GETALL_LIMIT);
|
|
52
|
+
const chunkResults = await Promise.all(
|
|
53
|
+
chunks.map((chunk) => {
|
|
54
|
+
const refs = chunk.map((id) => db.collection(collection).doc(id));
|
|
55
|
+
return db.getAll(...refs, { fieldMask });
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
return chunkResults.flat().map((snap) => adaptSnapshot(snap, fieldMask));
|
|
59
|
+
},
|
|
60
|
+
async getCollection(collection, filters, fieldMask, orderBy, limit) {
|
|
61
|
+
let query = db.collection(collection);
|
|
62
|
+
for (const filter of filters) {
|
|
63
|
+
query = query.where(
|
|
64
|
+
filter.field,
|
|
65
|
+
filter.operator,
|
|
66
|
+
filter.value
|
|
67
|
+
);
|
|
91
68
|
}
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) {
|
|
101
|
-
word += input[i];
|
|
102
|
-
i++;
|
|
69
|
+
if (orderBy !== void 0) {
|
|
70
|
+
query = query.orderBy(orderBy.field, orderBy.direction);
|
|
71
|
+
}
|
|
72
|
+
if (limit !== void 0) {
|
|
73
|
+
query = query.limit(limit);
|
|
74
|
+
}
|
|
75
|
+
if (fieldMask.length > 0) {
|
|
76
|
+
query = query.select(...fieldMask);
|
|
103
77
|
}
|
|
104
|
-
|
|
105
|
-
|
|
78
|
+
const snapshot = await query.get();
|
|
79
|
+
return snapshot.docs.map((snap) => adaptSnapshot(snap, fieldMask));
|
|
106
80
|
}
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
tokens.push({ kind: "eof", value: "", position: i });
|
|
110
|
-
return tokens;
|
|
81
|
+
};
|
|
111
82
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return token;
|
|
127
|
-
}
|
|
128
|
-
consumeWord(expectedVal) {
|
|
129
|
-
const token = this.consume("word");
|
|
130
|
-
if (expectedVal !== void 0 && token.value !== expectedVal) {
|
|
131
|
-
throw new ParseError(`expected ${expectedVal}, got ${token.value}`, token.position);
|
|
132
|
-
}
|
|
133
|
-
return token;
|
|
134
|
-
}
|
|
135
|
-
parseQuery() {
|
|
136
|
-
if (this.peek().kind === "word" && this.peek().value === "query") {
|
|
137
|
-
this.consume("word");
|
|
138
|
-
}
|
|
139
|
-
this.consume("lbrace");
|
|
140
|
-
const node = this.parseModelSelection();
|
|
141
|
-
this.consume("rbrace");
|
|
142
|
-
if (this.peek().kind !== "eof") {
|
|
143
|
-
throw new ParseError("expected end of query", this.peek().position);
|
|
144
|
-
}
|
|
145
|
-
return node;
|
|
146
|
-
}
|
|
147
|
-
parseModelSelection() {
|
|
148
|
-
const modelToken = this.consumeWord();
|
|
149
|
-
const model = modelToken.value;
|
|
150
|
-
this.consume("lparen");
|
|
151
|
-
this.consumeWord("id");
|
|
152
|
-
this.consume("colon");
|
|
153
|
-
const idToken = this.consume("string");
|
|
154
|
-
this.consume("rparen");
|
|
155
|
-
this.consume("lbrace");
|
|
156
|
-
const selections = this.parseFieldList();
|
|
157
|
-
this.consume("rbrace");
|
|
158
|
-
return { model, id: idToken.value, selections };
|
|
159
|
-
}
|
|
160
|
-
parseFieldList() {
|
|
161
|
-
const fields = [];
|
|
162
|
-
while (this.peek().kind !== "rbrace" && this.peek().kind !== "eof") {
|
|
163
|
-
fields.push(this.parseField());
|
|
164
|
-
}
|
|
165
|
-
if (fields.length === 0) {
|
|
166
|
-
throw new ParseError("selection set cannot be empty", this.peek().position);
|
|
167
|
-
}
|
|
168
|
-
return fields;
|
|
169
|
-
}
|
|
170
|
-
parseField() {
|
|
171
|
-
const nameToken = this.consumeWord();
|
|
172
|
-
const name = nameToken.value;
|
|
173
|
-
if (this.peek().kind === "lbrace") {
|
|
174
|
-
this.consume("lbrace");
|
|
175
|
-
const children = this.parseFieldList();
|
|
176
|
-
this.consume("rbrace");
|
|
177
|
-
return { name, children };
|
|
83
|
+
function adaptSnapshot(snap, fieldMask) {
|
|
84
|
+
return {
|
|
85
|
+
id: snap.id,
|
|
86
|
+
exists: snap.exists,
|
|
87
|
+
data() {
|
|
88
|
+
if (!snap.exists) return void 0;
|
|
89
|
+
const raw = snap.data() ?? {};
|
|
90
|
+
const masked = {};
|
|
91
|
+
for (const field of fieldMask) {
|
|
92
|
+
if (field in raw) {
|
|
93
|
+
masked[field] = raw[field];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return masked;
|
|
178
97
|
}
|
|
179
|
-
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function chunkArray(arr, size) {
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
103
|
+
chunks.push(arr.slice(i, i + size));
|
|
180
104
|
}
|
|
181
|
-
|
|
182
|
-
function parseQuery(input) {
|
|
183
|
-
const token = tokenize(input);
|
|
184
|
-
const parse = new Parser(token);
|
|
185
|
-
return parse.parseQuery();
|
|
105
|
+
return chunks;
|
|
186
106
|
}
|
|
187
107
|
|
|
188
|
-
// ../core/
|
|
108
|
+
// ../core/dist/index.js
|
|
189
109
|
var PlanError = class extends Error {
|
|
190
110
|
constructor(message) {
|
|
191
111
|
super(`PlanError: ${message}`);
|
|
@@ -211,13 +131,7 @@ function resolveSelections(selections, model, models, ctx, allOps) {
|
|
|
211
131
|
`relation field '${selection.name}' must have a selection set`
|
|
212
132
|
);
|
|
213
133
|
}
|
|
214
|
-
const dependent = planRelationField(
|
|
215
|
-
selection,
|
|
216
|
-
fieldDef,
|
|
217
|
-
models,
|
|
218
|
-
ctx,
|
|
219
|
-
allOps
|
|
220
|
-
);
|
|
134
|
+
const dependent = planRelationField(selection, fieldDef, models, ctx, allOps);
|
|
221
135
|
dependents.push(dependent);
|
|
222
136
|
} else {
|
|
223
137
|
scalarMask.push(selection.name);
|
|
@@ -319,8 +233,86 @@ function buildExecutionPlan(queryNode, models, ctx) {
|
|
|
319
233
|
);
|
|
320
234
|
return { root: rootOp, ops: allOps };
|
|
321
235
|
}
|
|
322
|
-
|
|
323
|
-
|
|
236
|
+
var INEQUALITY_OPS = /* @__PURE__ */ new Set(["!=", "<", "<=", ">", ">="]);
|
|
237
|
+
function validateFirestoreConstraints(filters, orderBy) {
|
|
238
|
+
const inequalityFields = [
|
|
239
|
+
...new Set(
|
|
240
|
+
filters.filter((f) => INEQUALITY_OPS.has(f.operator)).map((f) => f.field)
|
|
241
|
+
)
|
|
242
|
+
];
|
|
243
|
+
if (inequalityFields.length > 1) {
|
|
244
|
+
throw new PlanError(
|
|
245
|
+
`Firestore only allows inequality filters (!=, <, <=, >, >=) on ONE field per query.
|
|
246
|
+
You have inequality filters on: [${inequalityFields.join(", ")}].
|
|
247
|
+
Fix: keep one inequality field in .filter() and handle the rest in memory after .get().`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (inequalityFields.length === 1 && orderBy !== void 0 && orderBy.field !== inequalityFields[0]) {
|
|
251
|
+
throw new PlanError(
|
|
252
|
+
`Firestore requires orderBy to be on the same field as the inequality filter.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function validateModelFields(filters, selections, model) {
|
|
257
|
+
const availableFields = Object.keys(model.fields).join(", ");
|
|
258
|
+
for (const filter of filters) {
|
|
259
|
+
const fieldDef = model.fields[filter.field];
|
|
260
|
+
if (fieldDef === void 0) {
|
|
261
|
+
throw new PlanError(
|
|
262
|
+
`filter field '${filter.field}' does not exist on model '${model.source.path}'.
|
|
263
|
+
Available fields: [${availableFields}]`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (typeof fieldDef === "object" && "relation" in fieldDef) {
|
|
267
|
+
throw new PlanError(
|
|
268
|
+
`Use the scalar foreign key field instead (e.g. '${fieldDef.relation.from}').`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const selection of selections) {
|
|
273
|
+
if (model.fields[selection.name] === void 0) {
|
|
274
|
+
throw new PlanError(
|
|
275
|
+
`selected field '${selection.name}' does not exist on model '${model.source.path}'.
|
|
276
|
+
`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function buildCollectionPlan(queryNode, models, ctx) {
|
|
282
|
+
const model = models.get(queryNode.model);
|
|
283
|
+
if (model === void 0) {
|
|
284
|
+
throw new PlanError(`model '${queryNode.model}' is not registered`);
|
|
285
|
+
}
|
|
286
|
+
if (model.auth !== void 0 && !model.auth(ctx)) {
|
|
287
|
+
throw new PlanError(`unauthorized access to model '${queryNode.model}'`);
|
|
288
|
+
}
|
|
289
|
+
validateModelFields(queryNode.filters, queryNode.selections, model);
|
|
290
|
+
validateFirestoreConstraints(queryNode.filters, queryNode.orderBy);
|
|
291
|
+
const allOps = [];
|
|
292
|
+
const { scalarMask, dependents } = resolveSelections(
|
|
293
|
+
queryNode.selections,
|
|
294
|
+
model,
|
|
295
|
+
models,
|
|
296
|
+
ctx,
|
|
297
|
+
allOps
|
|
298
|
+
);
|
|
299
|
+
const relationForeignKeys = dependents.map((d) => d.foreignKey);
|
|
300
|
+
const fieldMask = Array.from(/* @__PURE__ */ new Set([...scalarMask, ...relationForeignKeys]));
|
|
301
|
+
const plan = {
|
|
302
|
+
collection: model.source.path,
|
|
303
|
+
fieldMask,
|
|
304
|
+
filters: queryNode.filters,
|
|
305
|
+
dependents,
|
|
306
|
+
selections: queryNode.selections
|
|
307
|
+
};
|
|
308
|
+
if (queryNode.orderBy !== void 0) {
|
|
309
|
+
plan.orderBy = queryNode.orderBy;
|
|
310
|
+
}
|
|
311
|
+
if (queryNode.limit !== void 0) {
|
|
312
|
+
plan.limit = queryNode.limit;
|
|
313
|
+
}
|
|
314
|
+
return plan;
|
|
315
|
+
}
|
|
324
316
|
var ExecutionError = class extends Error {
|
|
325
317
|
constructor(message) {
|
|
326
318
|
super(`ExecutionError: ${message}`);
|
|
@@ -402,7 +394,7 @@ async function executeOp(op, parentDoc, adapter) {
|
|
|
402
394
|
}
|
|
403
395
|
return executeGetMany(op, parentDoc, adapter);
|
|
404
396
|
}
|
|
405
|
-
async function
|
|
397
|
+
async function executePlan(plan, adapter) {
|
|
406
398
|
try {
|
|
407
399
|
const data = await executeOp(plan.root, null, adapter);
|
|
408
400
|
return { data };
|
|
@@ -413,70 +405,276 @@ async function executeplan(plan, adapter) {
|
|
|
413
405
|
throw err;
|
|
414
406
|
}
|
|
415
407
|
}
|
|
408
|
+
async function resolveCollectionDoc(snap, plan, adapter) {
|
|
409
|
+
if (!snap.exists) return null;
|
|
410
|
+
const docData = snap.data();
|
|
411
|
+
if (docData === void 0) return null;
|
|
412
|
+
const result = await resolveDependents(plan.dependents, docData, adapter);
|
|
413
|
+
const foreignKeys = new Set(plan.dependents.map((d) => d.foreignKey));
|
|
414
|
+
for (const [key, value] of Object.entries(docData)) {
|
|
415
|
+
if (!foreignKeys.has(key) && plan.fieldMask.includes(key)) {
|
|
416
|
+
result[key] = value;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
result.__id = snap.id;
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
async function executeCollectionPlan(plan, adapter) {
|
|
423
|
+
try {
|
|
424
|
+
const snapshots = await adapter.getCollection(
|
|
425
|
+
plan.collection,
|
|
426
|
+
plan.filters,
|
|
427
|
+
plan.fieldMask,
|
|
428
|
+
plan.orderBy,
|
|
429
|
+
plan.limit
|
|
430
|
+
);
|
|
431
|
+
const results = await Promise.all(
|
|
432
|
+
snapshots.map((snap) => resolveCollectionDoc(snap, plan, adapter))
|
|
433
|
+
);
|
|
434
|
+
const data = results.filter((r) => r !== null);
|
|
435
|
+
return { data };
|
|
436
|
+
} catch (err) {
|
|
437
|
+
if (err instanceof ExecutionError) {
|
|
438
|
+
return { data: [], errors: [err.message] };
|
|
439
|
+
}
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
416
443
|
|
|
417
|
-
// src/
|
|
418
|
-
var
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
444
|
+
// src/builder.ts
|
|
445
|
+
var CollectionReferenceImpl = class {
|
|
446
|
+
constructor(modelName, models, adapter) {
|
|
447
|
+
this.modelName = modelName;
|
|
448
|
+
this.models = models;
|
|
449
|
+
this.adapter = adapter;
|
|
450
|
+
}
|
|
451
|
+
doc(id) {
|
|
452
|
+
return new DocumentReferenceImpl(
|
|
453
|
+
this.modelName,
|
|
454
|
+
id,
|
|
455
|
+
this.models,
|
|
456
|
+
this.adapter
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
filter(field) {
|
|
460
|
+
return new FilterBuilderImpl(
|
|
461
|
+
field,
|
|
462
|
+
this.modelName,
|
|
463
|
+
[],
|
|
464
|
+
[],
|
|
465
|
+
void 0,
|
|
466
|
+
void 0,
|
|
467
|
+
this.models,
|
|
468
|
+
this.adapter
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
var DocumentReferenceImpl = class {
|
|
473
|
+
constructor(modelName, docId, models, adapter) {
|
|
474
|
+
this.modelName = modelName;
|
|
475
|
+
this.docId = docId;
|
|
476
|
+
this.models = models;
|
|
477
|
+
this.adapter = adapter;
|
|
478
|
+
}
|
|
479
|
+
select(...fields) {
|
|
480
|
+
return new QueryImpl(
|
|
481
|
+
this.modelName,
|
|
482
|
+
this.docId,
|
|
483
|
+
fields,
|
|
484
|
+
this.models,
|
|
485
|
+
this.adapter
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
var QueryImpl = class {
|
|
490
|
+
constructor(modelName, docId, fields, models, adapter) {
|
|
491
|
+
this.modelName = modelName;
|
|
492
|
+
this.docId = docId;
|
|
493
|
+
this.fields = fields;
|
|
494
|
+
this.models = models;
|
|
495
|
+
this.adapter = adapter;
|
|
496
|
+
}
|
|
497
|
+
async get(ctx) {
|
|
498
|
+
const queryNode = buildQueryNode(this.modelName, this.docId, this.fields);
|
|
499
|
+
const plan = buildExecutionPlan(queryNode, this.models, ctx);
|
|
500
|
+
return executePlan(plan, this.adapter);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
var FilterBuilderImpl = class {
|
|
504
|
+
constructor(field, modelName, existingFilters, selections, orderByClause, limitValue, models, adapter) {
|
|
505
|
+
this.field = field;
|
|
506
|
+
this.modelName = modelName;
|
|
507
|
+
this.existingFilters = existingFilters;
|
|
508
|
+
this.selections = selections;
|
|
509
|
+
this.orderByClause = orderByClause;
|
|
510
|
+
this.limitValue = limitValue;
|
|
511
|
+
this.models = models;
|
|
512
|
+
this.adapter = adapter;
|
|
513
|
+
}
|
|
514
|
+
next(operator, value) {
|
|
515
|
+
const newFilter = { field: this.field, operator, value };
|
|
516
|
+
return new CollectionQueryImpl(
|
|
517
|
+
this.modelName,
|
|
518
|
+
[...this.existingFilters, newFilter],
|
|
519
|
+
this.selections,
|
|
520
|
+
this.orderByClause,
|
|
521
|
+
this.limitValue,
|
|
522
|
+
this.models,
|
|
523
|
+
this.adapter
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
equals(value) {
|
|
527
|
+
return this.next("==", value);
|
|
528
|
+
}
|
|
529
|
+
not(value) {
|
|
530
|
+
return this.next("!=", value);
|
|
531
|
+
}
|
|
532
|
+
gt(value) {
|
|
533
|
+
return this.next(">", value);
|
|
534
|
+
}
|
|
535
|
+
gte(value) {
|
|
536
|
+
return this.next(">=", value);
|
|
537
|
+
}
|
|
538
|
+
lt(value) {
|
|
539
|
+
return this.next("<", value);
|
|
540
|
+
}
|
|
541
|
+
lte(value) {
|
|
542
|
+
return this.next("<=", value);
|
|
543
|
+
}
|
|
544
|
+
in(values) {
|
|
545
|
+
return this.next("in", values);
|
|
546
|
+
}
|
|
547
|
+
contains(value) {
|
|
548
|
+
return this.next("array-contains", value);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
var CollectionQueryImpl = class _CollectionQueryImpl {
|
|
552
|
+
constructor(modelName, filters, selectedFields, orderByClause, limitValue, models, adapter) {
|
|
553
|
+
this.modelName = modelName;
|
|
554
|
+
this.filters = filters;
|
|
555
|
+
this.selectedFields = selectedFields;
|
|
556
|
+
this.orderByClause = orderByClause;
|
|
557
|
+
this.limitValue = limitValue;
|
|
558
|
+
this.models = models;
|
|
559
|
+
this.adapter = adapter;
|
|
560
|
+
}
|
|
561
|
+
filter(field) {
|
|
562
|
+
return new FilterBuilderImpl(
|
|
563
|
+
field,
|
|
564
|
+
this.modelName,
|
|
565
|
+
this.filters,
|
|
566
|
+
this.selectedFields,
|
|
567
|
+
this.orderByClause,
|
|
568
|
+
this.limitValue,
|
|
569
|
+
this.models,
|
|
570
|
+
this.adapter
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
select(...fields) {
|
|
574
|
+
return new _CollectionQueryImpl(
|
|
575
|
+
this.modelName,
|
|
576
|
+
this.filters,
|
|
577
|
+
fields,
|
|
578
|
+
this.orderByClause,
|
|
579
|
+
this.limitValue,
|
|
580
|
+
this.models,
|
|
581
|
+
this.adapter
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
orderBy(field, direction) {
|
|
585
|
+
return new _CollectionQueryImpl(
|
|
586
|
+
this.modelName,
|
|
587
|
+
this.filters,
|
|
588
|
+
this.selectedFields,
|
|
589
|
+
{ field, direction },
|
|
590
|
+
this.limitValue,
|
|
591
|
+
this.models,
|
|
592
|
+
this.adapter
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
limit(n) {
|
|
596
|
+
return new _CollectionQueryImpl(
|
|
597
|
+
this.modelName,
|
|
598
|
+
this.filters,
|
|
599
|
+
this.selectedFields,
|
|
600
|
+
this.orderByClause,
|
|
601
|
+
n,
|
|
602
|
+
this.models,
|
|
603
|
+
this.adapter
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
async get(ctx) {
|
|
607
|
+
const queryNode = {
|
|
608
|
+
model: this.modelName,
|
|
609
|
+
filters: this.filters,
|
|
610
|
+
selections: parseFieldSelections(this.selectedFields)
|
|
611
|
+
};
|
|
612
|
+
if (this.orderByClause !== void 0) {
|
|
613
|
+
queryNode.orderBy = this.orderByClause;
|
|
436
614
|
}
|
|
437
|
-
|
|
615
|
+
if (this.limitValue !== void 0) {
|
|
616
|
+
queryNode.limit = this.limitValue;
|
|
617
|
+
}
|
|
618
|
+
const plan = buildCollectionPlan(queryNode, this.models, ctx);
|
|
619
|
+
return executeCollectionPlan(plan, this.adapter);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
function buildQueryNode(modelName, id, fields) {
|
|
623
|
+
const selections = parseFieldSelections(fields);
|
|
624
|
+
return { model: modelName, id, selections };
|
|
438
625
|
}
|
|
439
|
-
function
|
|
440
|
-
return
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
626
|
+
function addNestedField(parent, parts) {
|
|
627
|
+
if (parts.length === 0) return;
|
|
628
|
+
const [current, ...rest] = parts;
|
|
629
|
+
const name = current;
|
|
630
|
+
let child = parent.children.find((c) => c.name === name);
|
|
631
|
+
if (!child) {
|
|
632
|
+
child = { name, children: [] };
|
|
633
|
+
parent.children.push(child);
|
|
634
|
+
}
|
|
635
|
+
if (rest.length > 0) {
|
|
636
|
+
addNestedField(child, rest);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function parseFieldSelections(fields) {
|
|
640
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const field of fields) {
|
|
642
|
+
const parts = field.split(".");
|
|
643
|
+
if (parts.length === 1) {
|
|
644
|
+
const name = parts[0];
|
|
645
|
+
if (!fieldMap.has(name)) {
|
|
646
|
+
fieldMap.set(name, { name, children: [] });
|
|
451
647
|
}
|
|
452
|
-
|
|
648
|
+
} else {
|
|
649
|
+
const [root, ...rest] = parts;
|
|
650
|
+
const rootName = root;
|
|
651
|
+
if (!fieldMap.has(rootName)) {
|
|
652
|
+
fieldMap.set(rootName, { name: rootName, children: [] });
|
|
653
|
+
}
|
|
654
|
+
const rootField = fieldMap.get(rootName);
|
|
655
|
+
addNestedField(rootField, rest);
|
|
453
656
|
}
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
function chunkArray(arr, size) {
|
|
457
|
-
const chunks = [];
|
|
458
|
-
for (let i = 0; i < arr.length; i += size) {
|
|
459
|
-
chunks.push(arr.slice(i, i + size));
|
|
460
657
|
}
|
|
461
|
-
return
|
|
658
|
+
return Array.from(fieldMap.values());
|
|
659
|
+
}
|
|
660
|
+
function createCollectionRef(modelName, models, adapter) {
|
|
661
|
+
return new CollectionReferenceImpl(modelName, models, adapter);
|
|
462
662
|
}
|
|
463
663
|
|
|
464
664
|
// src/app.ts
|
|
465
665
|
function createServerlessApp(options) {
|
|
466
|
-
const { firestore
|
|
666
|
+
const { firestore } = options;
|
|
467
667
|
const models = /* @__PURE__ */ new Map();
|
|
468
668
|
const adapter = createFirestoreAdapter(firestore);
|
|
469
669
|
return {
|
|
470
670
|
model(name, definition) {
|
|
471
671
|
if (models.has(name)) {
|
|
472
|
-
|
|
672
|
+
console.warn(`[FlareQuery] model '${name}' is already registered \u2014 skipping duplicate registration`);
|
|
473
673
|
}
|
|
474
674
|
models.set(name, definition);
|
|
475
675
|
},
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const plan = buildExecutionPlan(queryNode, models, ctx);
|
|
479
|
-
return executeplan(plan, adapter);
|
|
676
|
+
collection(name) {
|
|
677
|
+
return createCollectionRef(name, models, adapter);
|
|
480
678
|
}
|
|
481
679
|
};
|
|
482
680
|
}
|
|
@@ -491,7 +689,11 @@ async function extractContext(authorization, auth) {
|
|
|
491
689
|
userId: decoded.uid,
|
|
492
690
|
token: decoded
|
|
493
691
|
};
|
|
494
|
-
} catch {
|
|
692
|
+
} catch (err) {
|
|
693
|
+
console.warn(
|
|
694
|
+
"[FlareQuery] extractContext: token verification failed \u2014",
|
|
695
|
+
err instanceof Error ? err.message : "unknown error"
|
|
696
|
+
);
|
|
495
697
|
return { userId: null, token: null };
|
|
496
698
|
}
|
|
497
699
|
}
|
|
@@ -499,66 +701,106 @@ async function extractContext(authorization, auth) {
|
|
|
499
701
|
// src/function.ts
|
|
500
702
|
var functionsV1 = __toESM(require("firebase-functions/v1"), 1);
|
|
501
703
|
var import_https = require("firebase-functions/v2/https");
|
|
704
|
+
var VALID_FIELD = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
705
|
+
function extractQueryRequest(body) {
|
|
706
|
+
if (typeof body !== "object" || body === null || !("model" in body) || !("id" in body) || !("select" in body)) return null;
|
|
707
|
+
const req = body;
|
|
708
|
+
if (typeof req.model !== "string" || typeof req.id !== "string" || !Array.isArray(req.select) || req.select.length === 0) return null;
|
|
709
|
+
const invalidField = req.select.find(
|
|
710
|
+
(f) => typeof f !== "string" || !VALID_FIELD.test(f)
|
|
711
|
+
);
|
|
712
|
+
if (invalidField !== void 0) {
|
|
713
|
+
return { error: `invalid field: "${String(invalidField)}"` };
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
model: req.model,
|
|
717
|
+
id: req.id,
|
|
718
|
+
select: req.select
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function handleCors(req, res, options) {
|
|
722
|
+
if (!options.cors) return false;
|
|
723
|
+
res.set("Access-Control-Allow-Origin", "*");
|
|
724
|
+
res.set("Access-Control-Allow-Methods", "POST");
|
|
725
|
+
res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
|
726
|
+
if (req.method === "OPTIONS") {
|
|
727
|
+
res.status(204).send("");
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
502
732
|
function createFunction(app, auth, options = {}) {
|
|
503
733
|
return functionsV1.https.onRequest(async (req, res) => {
|
|
504
|
-
if (options
|
|
505
|
-
res.set("Access-Control-Allow-Origin", "*");
|
|
506
|
-
res.set("Access-Control-Allow-Methods", "POST");
|
|
507
|
-
res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
|
508
|
-
if (req.method === "OPTIONS") {
|
|
509
|
-
res.status(204).send("");
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
734
|
+
if (handleCors(req, res, options)) return;
|
|
513
735
|
if (req.method !== "POST") {
|
|
514
736
|
res.status(405).json({ error: "method not allowed \u2014 use POST" });
|
|
515
737
|
return;
|
|
516
738
|
}
|
|
517
|
-
const
|
|
518
|
-
if (
|
|
519
|
-
res.status(400).json({
|
|
739
|
+
const queryReq = extractQueryRequest(req.body);
|
|
740
|
+
if (queryReq === null) {
|
|
741
|
+
res.status(400).json({
|
|
742
|
+
error: "request body must contain: { model, id, select }",
|
|
743
|
+
example: {
|
|
744
|
+
model: "Event",
|
|
745
|
+
id: "event_1",
|
|
746
|
+
select: ["title", "participants.name"]
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if ("error" in queryReq) {
|
|
752
|
+
res.status(400).json({ error: queryReq.error });
|
|
520
753
|
return;
|
|
521
754
|
}
|
|
522
755
|
const ctx = await extractContext(req.headers.authorization, auth);
|
|
523
|
-
|
|
524
|
-
|
|
756
|
+
try {
|
|
757
|
+
const response = await app.collection(queryReq.model).doc(queryReq.id).select(...queryReq.select).get(ctx);
|
|
758
|
+
res.status(200).json(response);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
res.status(500).json({
|
|
761
|
+
error: error instanceof Error ? error.message : "internal server error"
|
|
762
|
+
});
|
|
763
|
+
}
|
|
525
764
|
});
|
|
526
765
|
}
|
|
527
766
|
function createOnRequest(app, auth, options = {}) {
|
|
528
767
|
return (0, import_https.onRequest)(async (req, res) => {
|
|
529
|
-
if (options
|
|
530
|
-
res.set("Access-Control-Allow-Origin", "*");
|
|
531
|
-
res.set("Access-Control-Allow-Methods", "POST");
|
|
532
|
-
res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
|
533
|
-
if (req.method === "OPTIONS") {
|
|
534
|
-
res.status(204).send("");
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
768
|
+
if (handleCors(req, res, options)) return;
|
|
538
769
|
if (req.method !== "POST") {
|
|
539
770
|
res.status(405).json({ error: "method not allowed \u2014 use POST" });
|
|
540
771
|
return;
|
|
541
772
|
}
|
|
542
|
-
const
|
|
543
|
-
if (
|
|
544
|
-
res.status(400).json({
|
|
773
|
+
const queryReq = extractQueryRequest(req.body);
|
|
774
|
+
if (queryReq === null) {
|
|
775
|
+
res.status(400).json({
|
|
776
|
+
error: "request body must contain: { model, id, select }",
|
|
777
|
+
example: {
|
|
778
|
+
model: "Event",
|
|
779
|
+
id: "event_1",
|
|
780
|
+
select: ["title", "participants.name"]
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if ("error" in queryReq) {
|
|
786
|
+
res.status(400).json({ error: queryReq.error });
|
|
545
787
|
return;
|
|
546
788
|
}
|
|
547
789
|
const ctx = await extractContext(req.headers.authorization, auth);
|
|
548
|
-
|
|
549
|
-
|
|
790
|
+
try {
|
|
791
|
+
const response = await app.collection(queryReq.model).doc(queryReq.id).select(...queryReq.select).get(ctx);
|
|
792
|
+
res.status(200).json(response);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
res.status(500).json({
|
|
795
|
+
error: error instanceof Error ? error.message : "internal server error"
|
|
796
|
+
});
|
|
797
|
+
}
|
|
550
798
|
});
|
|
551
799
|
}
|
|
552
|
-
function extractQuery(body) {
|
|
553
|
-
if (typeof body === "string" && body.trim().length > 0) return body.trim();
|
|
554
|
-
if (typeof body === "object" && body !== null && "query" in body && typeof body["query"] === "string") {
|
|
555
|
-
return body["query"].trim();
|
|
556
|
-
}
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
800
|
// Annotate the CommonJS export names for ESM import in node:
|
|
560
801
|
0 && (module.exports = {
|
|
561
802
|
createFunction,
|
|
562
803
|
createOnRequest,
|
|
563
|
-
createServerlessApp
|
|
804
|
+
createServerlessApp,
|
|
805
|
+
extractContext
|
|
564
806
|
});
|