@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/dist/index.js CHANGED
@@ -1,153 +1,72 @@
1
- // ../core/src/parser/index.ts
2
- var ParseError = class extends Error {
3
- constructor(message, position) {
4
- super(`Parse error at ${position}: ${message}`);
5
- this.position = position;
6
- this.name = "ParseError";
7
- }
8
- };
9
- function tokenize(input) {
10
- const tokens = [];
11
- let i = 0;
12
- while (i < input.length) {
13
- if (/\s/.test(input[i]) || input[i] === ",") {
14
- i++;
15
- continue;
16
- }
17
- if (input[i] === "#") {
18
- while (i < input.length && input[i] !== "\n") i++;
19
- continue;
20
- }
21
- if (input[i] === "{") {
22
- tokens.push({ kind: "lbrace", value: "{", position: i });
23
- i++;
24
- continue;
25
- }
26
- if (input[i] === "}") {
27
- tokens.push({ kind: "rbrace", value: "}", position: i });
28
- i++;
29
- continue;
30
- }
31
- if (input[i] === "(") {
32
- tokens.push({ kind: "lparen", value: "(", position: i });
33
- i++;
34
- continue;
35
- }
36
- if (input[i] === ")") {
37
- tokens.push({ kind: "rparen", value: ")", position: i });
38
- i++;
39
- continue;
40
- }
41
- if (input[i] === ":") {
42
- tokens.push({ kind: "colon", value: ":", position: i });
43
- i++;
44
- continue;
45
- }
46
- if (input[i] === '"') {
47
- const start = i;
48
- i++;
49
- let str = "";
50
- while (i < input.length && input[i] !== '"') {
51
- str += input[i];
52
- i++;
1
+ // src/adapter.ts
2
+ var FIRESTORE_GETALL_LIMIT = 300;
3
+ function createFirestoreAdapter(db) {
4
+ return {
5
+ async getOne(collection, id, fieldMask) {
6
+ const ref = db.collection(collection).doc(id);
7
+ const snapshot = await ref.get();
8
+ return adaptSnapshot(snapshot, fieldMask);
9
+ },
10
+ async getMany(collection, ids, fieldMask) {
11
+ if (ids.length === 0) return [];
12
+ const chunks = chunkArray(ids, FIRESTORE_GETALL_LIMIT);
13
+ const chunkResults = await Promise.all(
14
+ chunks.map((chunk) => {
15
+ const refs = chunk.map((id) => db.collection(collection).doc(id));
16
+ return db.getAll(...refs, { fieldMask });
17
+ })
18
+ );
19
+ return chunkResults.flat().map((snap) => adaptSnapshot(snap, fieldMask));
20
+ },
21
+ async getCollection(collection, filters, fieldMask, orderBy, limit) {
22
+ let query = db.collection(collection);
23
+ for (const filter of filters) {
24
+ query = query.where(
25
+ filter.field,
26
+ filter.operator,
27
+ filter.value
28
+ );
53
29
  }
54
- if (i >= input.length) throw new ParseError("unterminated string literal", start);
55
- i++;
56
- tokens.push({ kind: "string", value: str, position: start });
57
- continue;
58
- }
59
- if (/[a-zA-Z_]/.test(input[i])) {
60
- const start = i;
61
- let word = "";
62
- while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) {
63
- word += input[i];
64
- i++;
30
+ if (orderBy !== void 0) {
31
+ query = query.orderBy(orderBy.field, orderBy.direction);
32
+ }
33
+ if (limit !== void 0) {
34
+ query = query.limit(limit);
35
+ }
36
+ if (fieldMask.length > 0) {
37
+ query = query.select(...fieldMask);
65
38
  }
66
- tokens.push({ kind: "word", value: word, position: start });
67
- continue;
39
+ const snapshot = await query.get();
40
+ return snapshot.docs.map((snap) => adaptSnapshot(snap, fieldMask));
68
41
  }
69
- throw new ParseError(`unexpected character '${input[i]}'`, i);
70
- }
71
- tokens.push({ kind: "eof", value: "", position: i });
72
- return tokens;
42
+ };
73
43
  }
74
- var Parser = class {
75
- constructor(tokens) {
76
- this.tokens = tokens;
77
- this.pos = 0;
78
- }
79
- peek() {
80
- return this.tokens[this.pos];
81
- }
82
- consume(expectedKind) {
83
- const token = this.tokens[this.pos];
84
- if (expectedKind && token.kind !== expectedKind) {
85
- throw new ParseError(`expected ${expectedKind}, got ${token.kind}`, token.position);
86
- }
87
- this.pos++;
88
- return token;
89
- }
90
- consumeWord(expectedVal) {
91
- const token = this.consume("word");
92
- if (expectedVal !== void 0 && token.value !== expectedVal) {
93
- throw new ParseError(`expected ${expectedVal}, got ${token.value}`, token.position);
94
- }
95
- return token;
96
- }
97
- parseQuery() {
98
- if (this.peek().kind === "word" && this.peek().value === "query") {
99
- this.consume("word");
100
- }
101
- this.consume("lbrace");
102
- const node = this.parseModelSelection();
103
- this.consume("rbrace");
104
- if (this.peek().kind !== "eof") {
105
- throw new ParseError("expected end of query", this.peek().position);
106
- }
107
- return node;
108
- }
109
- parseModelSelection() {
110
- const modelToken = this.consumeWord();
111
- const model = modelToken.value;
112
- this.consume("lparen");
113
- this.consumeWord("id");
114
- this.consume("colon");
115
- const idToken = this.consume("string");
116
- this.consume("rparen");
117
- this.consume("lbrace");
118
- const selections = this.parseFieldList();
119
- this.consume("rbrace");
120
- return { model, id: idToken.value, selections };
121
- }
122
- parseFieldList() {
123
- const fields = [];
124
- while (this.peek().kind !== "rbrace" && this.peek().kind !== "eof") {
125
- fields.push(this.parseField());
126
- }
127
- if (fields.length === 0) {
128
- throw new ParseError("selection set cannot be empty", this.peek().position);
129
- }
130
- return fields;
131
- }
132
- parseField() {
133
- const nameToken = this.consumeWord();
134
- const name = nameToken.value;
135
- if (this.peek().kind === "lbrace") {
136
- this.consume("lbrace");
137
- const children = this.parseFieldList();
138
- this.consume("rbrace");
139
- return { name, children };
44
+ function adaptSnapshot(snap, fieldMask) {
45
+ return {
46
+ id: snap.id,
47
+ exists: snap.exists,
48
+ data() {
49
+ if (!snap.exists) return void 0;
50
+ const raw = snap.data() ?? {};
51
+ const masked = {};
52
+ for (const field of fieldMask) {
53
+ if (field in raw) {
54
+ masked[field] = raw[field];
55
+ }
56
+ }
57
+ return masked;
140
58
  }
141
- return { name, children: [] };
59
+ };
60
+ }
61
+ function chunkArray(arr, size) {
62
+ const chunks = [];
63
+ for (let i = 0; i < arr.length; i += size) {
64
+ chunks.push(arr.slice(i, i + size));
142
65
  }
143
- };
144
- function parseQuery(input) {
145
- const token = tokenize(input);
146
- const parse = new Parser(token);
147
- return parse.parseQuery();
66
+ return chunks;
148
67
  }
149
68
 
150
- // ../core/src/planner/index.ts
69
+ // ../core/dist/index.js
151
70
  var PlanError = class extends Error {
152
71
  constructor(message) {
153
72
  super(`PlanError: ${message}`);
@@ -173,13 +92,7 @@ function resolveSelections(selections, model, models, ctx, allOps) {
173
92
  `relation field '${selection.name}' must have a selection set`
174
93
  );
175
94
  }
176
- const dependent = planRelationField(
177
- selection,
178
- fieldDef,
179
- models,
180
- ctx,
181
- allOps
182
- );
95
+ const dependent = planRelationField(selection, fieldDef, models, ctx, allOps);
183
96
  dependents.push(dependent);
184
97
  } else {
185
98
  scalarMask.push(selection.name);
@@ -281,8 +194,86 @@ function buildExecutionPlan(queryNode, models, ctx) {
281
194
  );
282
195
  return { root: rootOp, ops: allOps };
283
196
  }
284
-
285
- // ../core/src/executor/index.ts
197
+ var INEQUALITY_OPS = /* @__PURE__ */ new Set(["!=", "<", "<=", ">", ">="]);
198
+ function validateFirestoreConstraints(filters, orderBy) {
199
+ const inequalityFields = [
200
+ ...new Set(
201
+ filters.filter((f) => INEQUALITY_OPS.has(f.operator)).map((f) => f.field)
202
+ )
203
+ ];
204
+ if (inequalityFields.length > 1) {
205
+ throw new PlanError(
206
+ `Firestore only allows inequality filters (!=, <, <=, >, >=) on ONE field per query.
207
+ You have inequality filters on: [${inequalityFields.join(", ")}].
208
+ Fix: keep one inequality field in .filter() and handle the rest in memory after .get().`
209
+ );
210
+ }
211
+ if (inequalityFields.length === 1 && orderBy !== void 0 && orderBy.field !== inequalityFields[0]) {
212
+ throw new PlanError(
213
+ `Firestore requires orderBy to be on the same field as the inequality filter.`
214
+ );
215
+ }
216
+ }
217
+ function validateModelFields(filters, selections, model) {
218
+ const availableFields = Object.keys(model.fields).join(", ");
219
+ for (const filter of filters) {
220
+ const fieldDef = model.fields[filter.field];
221
+ if (fieldDef === void 0) {
222
+ throw new PlanError(
223
+ `filter field '${filter.field}' does not exist on model '${model.source.path}'.
224
+ Available fields: [${availableFields}]`
225
+ );
226
+ }
227
+ if (typeof fieldDef === "object" && "relation" in fieldDef) {
228
+ throw new PlanError(
229
+ `Use the scalar foreign key field instead (e.g. '${fieldDef.relation.from}').`
230
+ );
231
+ }
232
+ }
233
+ for (const selection of selections) {
234
+ if (model.fields[selection.name] === void 0) {
235
+ throw new PlanError(
236
+ `selected field '${selection.name}' does not exist on model '${model.source.path}'.
237
+ `
238
+ );
239
+ }
240
+ }
241
+ }
242
+ function buildCollectionPlan(queryNode, models, ctx) {
243
+ const model = models.get(queryNode.model);
244
+ if (model === void 0) {
245
+ throw new PlanError(`model '${queryNode.model}' is not registered`);
246
+ }
247
+ if (model.auth !== void 0 && !model.auth(ctx)) {
248
+ throw new PlanError(`unauthorized access to model '${queryNode.model}'`);
249
+ }
250
+ validateModelFields(queryNode.filters, queryNode.selections, model);
251
+ validateFirestoreConstraints(queryNode.filters, queryNode.orderBy);
252
+ const allOps = [];
253
+ const { scalarMask, dependents } = resolveSelections(
254
+ queryNode.selections,
255
+ model,
256
+ models,
257
+ ctx,
258
+ allOps
259
+ );
260
+ const relationForeignKeys = dependents.map((d) => d.foreignKey);
261
+ const fieldMask = Array.from(/* @__PURE__ */ new Set([...scalarMask, ...relationForeignKeys]));
262
+ const plan = {
263
+ collection: model.source.path,
264
+ fieldMask,
265
+ filters: queryNode.filters,
266
+ dependents,
267
+ selections: queryNode.selections
268
+ };
269
+ if (queryNode.orderBy !== void 0) {
270
+ plan.orderBy = queryNode.orderBy;
271
+ }
272
+ if (queryNode.limit !== void 0) {
273
+ plan.limit = queryNode.limit;
274
+ }
275
+ return plan;
276
+ }
286
277
  var ExecutionError = class extends Error {
287
278
  constructor(message) {
288
279
  super(`ExecutionError: ${message}`);
@@ -364,7 +355,7 @@ async function executeOp(op, parentDoc, adapter) {
364
355
  }
365
356
  return executeGetMany(op, parentDoc, adapter);
366
357
  }
367
- async function executeplan(plan, adapter) {
358
+ async function executePlan(plan, adapter) {
368
359
  try {
369
360
  const data = await executeOp(plan.root, null, adapter);
370
361
  return { data };
@@ -375,70 +366,276 @@ async function executeplan(plan, adapter) {
375
366
  throw err;
376
367
  }
377
368
  }
369
+ async function resolveCollectionDoc(snap, plan, adapter) {
370
+ if (!snap.exists) return null;
371
+ const docData = snap.data();
372
+ if (docData === void 0) return null;
373
+ const result = await resolveDependents(plan.dependents, docData, adapter);
374
+ const foreignKeys = new Set(plan.dependents.map((d) => d.foreignKey));
375
+ for (const [key, value] of Object.entries(docData)) {
376
+ if (!foreignKeys.has(key) && plan.fieldMask.includes(key)) {
377
+ result[key] = value;
378
+ }
379
+ }
380
+ result.__id = snap.id;
381
+ return result;
382
+ }
383
+ async function executeCollectionPlan(plan, adapter) {
384
+ try {
385
+ const snapshots = await adapter.getCollection(
386
+ plan.collection,
387
+ plan.filters,
388
+ plan.fieldMask,
389
+ plan.orderBy,
390
+ plan.limit
391
+ );
392
+ const results = await Promise.all(
393
+ snapshots.map((snap) => resolveCollectionDoc(snap, plan, adapter))
394
+ );
395
+ const data = results.filter((r) => r !== null);
396
+ return { data };
397
+ } catch (err) {
398
+ if (err instanceof ExecutionError) {
399
+ return { data: [], errors: [err.message] };
400
+ }
401
+ throw err;
402
+ }
403
+ }
378
404
 
379
- // src/adapter.ts
380
- var FIRESTORE_GETALL_LIMIT = 300;
381
- function createFirestoreAdapter(db) {
382
- return {
383
- async getOne(collection, id, fieldMask) {
384
- const ref = db.collection(collection).doc(id);
385
- const snapshot = await ref.get();
386
- return adaptSnapshot(snapshot, fieldMask);
387
- },
388
- async getMany(collection, ids, fieldMask) {
389
- if (ids.length === 0) return [];
390
- const chunks = chunkArray(ids, FIRESTORE_GETALL_LIMIT);
391
- const chunkResults = await Promise.all(
392
- chunks.map((chunk) => {
393
- const refs = chunk.map((id) => db.collection(collection).doc(id));
394
- return db.getAll(...refs, { fieldMask });
395
- })
396
- );
397
- return chunkResults.flat().map((snap) => adaptSnapshot(snap, fieldMask));
405
+ // src/builder.ts
406
+ var CollectionReferenceImpl = class {
407
+ constructor(modelName, models, adapter) {
408
+ this.modelName = modelName;
409
+ this.models = models;
410
+ this.adapter = adapter;
411
+ }
412
+ doc(id) {
413
+ return new DocumentReferenceImpl(
414
+ this.modelName,
415
+ id,
416
+ this.models,
417
+ this.adapter
418
+ );
419
+ }
420
+ filter(field) {
421
+ return new FilterBuilderImpl(
422
+ field,
423
+ this.modelName,
424
+ [],
425
+ [],
426
+ void 0,
427
+ void 0,
428
+ this.models,
429
+ this.adapter
430
+ );
431
+ }
432
+ };
433
+ var DocumentReferenceImpl = class {
434
+ constructor(modelName, docId, models, adapter) {
435
+ this.modelName = modelName;
436
+ this.docId = docId;
437
+ this.models = models;
438
+ this.adapter = adapter;
439
+ }
440
+ select(...fields) {
441
+ return new QueryImpl(
442
+ this.modelName,
443
+ this.docId,
444
+ fields,
445
+ this.models,
446
+ this.adapter
447
+ );
448
+ }
449
+ };
450
+ var QueryImpl = class {
451
+ constructor(modelName, docId, fields, models, adapter) {
452
+ this.modelName = modelName;
453
+ this.docId = docId;
454
+ this.fields = fields;
455
+ this.models = models;
456
+ this.adapter = adapter;
457
+ }
458
+ async get(ctx) {
459
+ const queryNode = buildQueryNode(this.modelName, this.docId, this.fields);
460
+ const plan = buildExecutionPlan(queryNode, this.models, ctx);
461
+ return executePlan(plan, this.adapter);
462
+ }
463
+ };
464
+ var FilterBuilderImpl = class {
465
+ constructor(field, modelName, existingFilters, selections, orderByClause, limitValue, models, adapter) {
466
+ this.field = field;
467
+ this.modelName = modelName;
468
+ this.existingFilters = existingFilters;
469
+ this.selections = selections;
470
+ this.orderByClause = orderByClause;
471
+ this.limitValue = limitValue;
472
+ this.models = models;
473
+ this.adapter = adapter;
474
+ }
475
+ next(operator, value) {
476
+ const newFilter = { field: this.field, operator, value };
477
+ return new CollectionQueryImpl(
478
+ this.modelName,
479
+ [...this.existingFilters, newFilter],
480
+ this.selections,
481
+ this.orderByClause,
482
+ this.limitValue,
483
+ this.models,
484
+ this.adapter
485
+ );
486
+ }
487
+ equals(value) {
488
+ return this.next("==", value);
489
+ }
490
+ not(value) {
491
+ return this.next("!=", value);
492
+ }
493
+ gt(value) {
494
+ return this.next(">", value);
495
+ }
496
+ gte(value) {
497
+ return this.next(">=", value);
498
+ }
499
+ lt(value) {
500
+ return this.next("<", value);
501
+ }
502
+ lte(value) {
503
+ return this.next("<=", value);
504
+ }
505
+ in(values) {
506
+ return this.next("in", values);
507
+ }
508
+ contains(value) {
509
+ return this.next("array-contains", value);
510
+ }
511
+ };
512
+ var CollectionQueryImpl = class _CollectionQueryImpl {
513
+ constructor(modelName, filters, selectedFields, orderByClause, limitValue, models, adapter) {
514
+ this.modelName = modelName;
515
+ this.filters = filters;
516
+ this.selectedFields = selectedFields;
517
+ this.orderByClause = orderByClause;
518
+ this.limitValue = limitValue;
519
+ this.models = models;
520
+ this.adapter = adapter;
521
+ }
522
+ filter(field) {
523
+ return new FilterBuilderImpl(
524
+ field,
525
+ this.modelName,
526
+ this.filters,
527
+ this.selectedFields,
528
+ this.orderByClause,
529
+ this.limitValue,
530
+ this.models,
531
+ this.adapter
532
+ );
533
+ }
534
+ select(...fields) {
535
+ return new _CollectionQueryImpl(
536
+ this.modelName,
537
+ this.filters,
538
+ fields,
539
+ this.orderByClause,
540
+ this.limitValue,
541
+ this.models,
542
+ this.adapter
543
+ );
544
+ }
545
+ orderBy(field, direction) {
546
+ return new _CollectionQueryImpl(
547
+ this.modelName,
548
+ this.filters,
549
+ this.selectedFields,
550
+ { field, direction },
551
+ this.limitValue,
552
+ this.models,
553
+ this.adapter
554
+ );
555
+ }
556
+ limit(n) {
557
+ return new _CollectionQueryImpl(
558
+ this.modelName,
559
+ this.filters,
560
+ this.selectedFields,
561
+ this.orderByClause,
562
+ n,
563
+ this.models,
564
+ this.adapter
565
+ );
566
+ }
567
+ async get(ctx) {
568
+ const queryNode = {
569
+ model: this.modelName,
570
+ filters: this.filters,
571
+ selections: parseFieldSelections(this.selectedFields)
572
+ };
573
+ if (this.orderByClause !== void 0) {
574
+ queryNode.orderBy = this.orderByClause;
398
575
  }
399
- };
576
+ if (this.limitValue !== void 0) {
577
+ queryNode.limit = this.limitValue;
578
+ }
579
+ const plan = buildCollectionPlan(queryNode, this.models, ctx);
580
+ return executeCollectionPlan(plan, this.adapter);
581
+ }
582
+ };
583
+ function buildQueryNode(modelName, id, fields) {
584
+ const selections = parseFieldSelections(fields);
585
+ return { model: modelName, id, selections };
400
586
  }
401
- function adaptSnapshot(snap, fieldMask) {
402
- return {
403
- id: snap.id,
404
- exists: snap.exists,
405
- data() {
406
- if (!snap.exists) return void 0;
407
- const raw = snap.data() ?? {};
408
- const masked = {};
409
- for (const field of fieldMask) {
410
- if (field in raw) {
411
- masked[field] = raw[field];
412
- }
587
+ function addNestedField(parent, parts) {
588
+ if (parts.length === 0) return;
589
+ const [current, ...rest] = parts;
590
+ const name = current;
591
+ let child = parent.children.find((c) => c.name === name);
592
+ if (!child) {
593
+ child = { name, children: [] };
594
+ parent.children.push(child);
595
+ }
596
+ if (rest.length > 0) {
597
+ addNestedField(child, rest);
598
+ }
599
+ }
600
+ function parseFieldSelections(fields) {
601
+ const fieldMap = /* @__PURE__ */ new Map();
602
+ for (const field of fields) {
603
+ const parts = field.split(".");
604
+ if (parts.length === 1) {
605
+ const name = parts[0];
606
+ if (!fieldMap.has(name)) {
607
+ fieldMap.set(name, { name, children: [] });
413
608
  }
414
- return masked;
609
+ } else {
610
+ const [root, ...rest] = parts;
611
+ const rootName = root;
612
+ if (!fieldMap.has(rootName)) {
613
+ fieldMap.set(rootName, { name: rootName, children: [] });
614
+ }
615
+ const rootField = fieldMap.get(rootName);
616
+ addNestedField(rootField, rest);
415
617
  }
416
- };
417
- }
418
- function chunkArray(arr, size) {
419
- const chunks = [];
420
- for (let i = 0; i < arr.length; i += size) {
421
- chunks.push(arr.slice(i, i + size));
422
618
  }
423
- return chunks;
619
+ return Array.from(fieldMap.values());
620
+ }
621
+ function createCollectionRef(modelName, models, adapter) {
622
+ return new CollectionReferenceImpl(modelName, models, adapter);
424
623
  }
425
624
 
426
625
  // src/app.ts
427
626
  function createServerlessApp(options) {
428
- const { firestore, auth: firebaseAuth } = options;
627
+ const { firestore } = options;
429
628
  const models = /* @__PURE__ */ new Map();
430
629
  const adapter = createFirestoreAdapter(firestore);
431
630
  return {
432
631
  model(name, definition) {
433
632
  if (models.has(name)) {
434
- throw new Error(`model '${name}' is already registered`);
633
+ console.warn(`[FlareQuery] model '${name}' is already registered \u2014 skipping duplicate registration`);
435
634
  }
436
635
  models.set(name, definition);
437
636
  },
438
- async execute(query, ctx) {
439
- const queryNode = parseQuery(query);
440
- const plan = buildExecutionPlan(queryNode, models, ctx);
441
- return executeplan(plan, adapter);
637
+ collection(name) {
638
+ return createCollectionRef(name, models, adapter);
442
639
  }
443
640
  };
444
641
  }
@@ -453,7 +650,11 @@ async function extractContext(authorization, auth) {
453
650
  userId: decoded.uid,
454
651
  token: decoded
455
652
  };
456
- } catch {
653
+ } catch (err) {
654
+ console.warn(
655
+ "[FlareQuery] extractContext: token verification failed \u2014",
656
+ err instanceof Error ? err.message : "unknown error"
657
+ );
457
658
  return { userId: null, token: null };
458
659
  }
459
660
  }
@@ -461,65 +662,105 @@ async function extractContext(authorization, auth) {
461
662
  // src/function.ts
462
663
  import * as functionsV1 from "firebase-functions/v1";
463
664
  import { onRequest } from "firebase-functions/v2/https";
665
+ var VALID_FIELD = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
666
+ function extractQueryRequest(body) {
667
+ if (typeof body !== "object" || body === null || !("model" in body) || !("id" in body) || !("select" in body)) return null;
668
+ const req = body;
669
+ if (typeof req.model !== "string" || typeof req.id !== "string" || !Array.isArray(req.select) || req.select.length === 0) return null;
670
+ const invalidField = req.select.find(
671
+ (f) => typeof f !== "string" || !VALID_FIELD.test(f)
672
+ );
673
+ if (invalidField !== void 0) {
674
+ return { error: `invalid field: "${String(invalidField)}"` };
675
+ }
676
+ return {
677
+ model: req.model,
678
+ id: req.id,
679
+ select: req.select
680
+ };
681
+ }
682
+ function handleCors(req, res, options) {
683
+ if (!options.cors) return false;
684
+ res.set("Access-Control-Allow-Origin", "*");
685
+ res.set("Access-Control-Allow-Methods", "POST");
686
+ res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
687
+ if (req.method === "OPTIONS") {
688
+ res.status(204).send("");
689
+ return true;
690
+ }
691
+ return false;
692
+ }
464
693
  function createFunction(app, auth, options = {}) {
465
694
  return functionsV1.https.onRequest(async (req, res) => {
466
- if (options.cors) {
467
- res.set("Access-Control-Allow-Origin", "*");
468
- res.set("Access-Control-Allow-Methods", "POST");
469
- res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
470
- if (req.method === "OPTIONS") {
471
- res.status(204).send("");
472
- return;
473
- }
474
- }
695
+ if (handleCors(req, res, options)) return;
475
696
  if (req.method !== "POST") {
476
697
  res.status(405).json({ error: "method not allowed \u2014 use POST" });
477
698
  return;
478
699
  }
479
- const query = extractQuery(req.body);
480
- if (query === null) {
481
- res.status(400).json({ error: "request body must contain a 'query' string field" });
700
+ const queryReq = extractQueryRequest(req.body);
701
+ if (queryReq === null) {
702
+ res.status(400).json({
703
+ error: "request body must contain: { model, id, select }",
704
+ example: {
705
+ model: "Event",
706
+ id: "event_1",
707
+ select: ["title", "participants.name"]
708
+ }
709
+ });
710
+ return;
711
+ }
712
+ if ("error" in queryReq) {
713
+ res.status(400).json({ error: queryReq.error });
482
714
  return;
483
715
  }
484
716
  const ctx = await extractContext(req.headers.authorization, auth);
485
- const response = await app.execute(query, ctx);
486
- res.status(200).json(response);
717
+ try {
718
+ const response = await app.collection(queryReq.model).doc(queryReq.id).select(...queryReq.select).get(ctx);
719
+ res.status(200).json(response);
720
+ } catch (error) {
721
+ res.status(500).json({
722
+ error: error instanceof Error ? error.message : "internal server error"
723
+ });
724
+ }
487
725
  });
488
726
  }
489
727
  function createOnRequest(app, auth, options = {}) {
490
728
  return onRequest(async (req, res) => {
491
- if (options.cors) {
492
- res.set("Access-Control-Allow-Origin", "*");
493
- res.set("Access-Control-Allow-Methods", "POST");
494
- res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
495
- if (req.method === "OPTIONS") {
496
- res.status(204).send("");
497
- return;
498
- }
499
- }
729
+ if (handleCors(req, res, options)) return;
500
730
  if (req.method !== "POST") {
501
731
  res.status(405).json({ error: "method not allowed \u2014 use POST" });
502
732
  return;
503
733
  }
504
- const query = extractQuery(req.body);
505
- if (query === null) {
506
- res.status(400).json({ error: "request body must contain a 'query' string field" });
734
+ const queryReq = extractQueryRequest(req.body);
735
+ if (queryReq === null) {
736
+ res.status(400).json({
737
+ error: "request body must contain: { model, id, select }",
738
+ example: {
739
+ model: "Event",
740
+ id: "event_1",
741
+ select: ["title", "participants.name"]
742
+ }
743
+ });
744
+ return;
745
+ }
746
+ if ("error" in queryReq) {
747
+ res.status(400).json({ error: queryReq.error });
507
748
  return;
508
749
  }
509
750
  const ctx = await extractContext(req.headers.authorization, auth);
510
- const response = await app.execute(query, ctx);
511
- res.status(200).json(response);
751
+ try {
752
+ const response = await app.collection(queryReq.model).doc(queryReq.id).select(...queryReq.select).get(ctx);
753
+ res.status(200).json(response);
754
+ } catch (error) {
755
+ res.status(500).json({
756
+ error: error instanceof Error ? error.message : "internal server error"
757
+ });
758
+ }
512
759
  });
513
760
  }
514
- function extractQuery(body) {
515
- if (typeof body === "string" && body.trim().length > 0) return body.trim();
516
- if (typeof body === "object" && body !== null && "query" in body && typeof body["query"] === "string") {
517
- return body["query"].trim();
518
- }
519
- return null;
520
- }
521
761
  export {
522
762
  createFunction,
523
763
  createOnRequest,
524
- createServerlessApp
764
+ createServerlessApp,
765
+ extractContext
525
766
  };