@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.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
- // ../core/src/parser/index.ts
40
- var ParseError = class extends Error {
41
- constructor(message, position) {
42
- super(`Parse error at ${position}: ${message}`);
43
- this.position = position;
44
- this.name = "ParseError";
45
- }
46
- };
47
- function tokenize(input) {
48
- const tokens = [];
49
- let i = 0;
50
- while (i < input.length) {
51
- if (/\s/.test(input[i]) || input[i] === ",") {
52
- i++;
53
- continue;
54
- }
55
- if (input[i] === "#") {
56
- while (i < input.length && input[i] !== "\n") i++;
57
- continue;
58
- }
59
- if (input[i] === "{") {
60
- tokens.push({ kind: "lbrace", value: "{", position: i });
61
- i++;
62
- continue;
63
- }
64
- if (input[i] === "}") {
65
- tokens.push({ kind: "rbrace", value: "}", position: i });
66
- i++;
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 (i >= input.length) throw new ParseError("unterminated string literal", start);
93
- i++;
94
- tokens.push({ kind: "string", value: str, position: start });
95
- continue;
96
- }
97
- if (/[a-zA-Z_]/.test(input[i])) {
98
- const start = i;
99
- let word = "";
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
- tokens.push({ kind: "word", value: word, position: start });
105
- continue;
78
+ const snapshot = await query.get();
79
+ return snapshot.docs.map((snap) => adaptSnapshot(snap, fieldMask));
106
80
  }
107
- throw new ParseError(`unexpected character '${input[i]}'`, i);
108
- }
109
- tokens.push({ kind: "eof", value: "", position: i });
110
- return tokens;
81
+ };
111
82
  }
112
- var Parser = class {
113
- constructor(tokens) {
114
- this.tokens = tokens;
115
- this.pos = 0;
116
- }
117
- peek() {
118
- return this.tokens[this.pos];
119
- }
120
- consume(expectedKind) {
121
- const token = this.tokens[this.pos];
122
- if (expectedKind && token.kind !== expectedKind) {
123
- throw new ParseError(`expected ${expectedKind}, got ${token.kind}`, token.position);
124
- }
125
- this.pos++;
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
- return { name, children: [] };
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/src/planner/index.ts
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
- // ../core/src/executor/index.ts
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 executeplan(plan, adapter) {
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/adapter.ts
418
- var FIRESTORE_GETALL_LIMIT = 300;
419
- function createFirestoreAdapter(db) {
420
- return {
421
- async getOne(collection, id, fieldMask) {
422
- const ref = db.collection(collection).doc(id);
423
- const snapshot = await ref.get();
424
- return adaptSnapshot(snapshot, fieldMask);
425
- },
426
- async getMany(collection, ids, fieldMask) {
427
- if (ids.length === 0) return [];
428
- const chunks = chunkArray(ids, FIRESTORE_GETALL_LIMIT);
429
- const chunkResults = await Promise.all(
430
- chunks.map((chunk) => {
431
- const refs = chunk.map((id) => db.collection(collection).doc(id));
432
- return db.getAll(...refs, { fieldMask });
433
- })
434
- );
435
- return chunkResults.flat().map((snap) => adaptSnapshot(snap, fieldMask));
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 adaptSnapshot(snap, fieldMask) {
440
- return {
441
- id: snap.id,
442
- exists: snap.exists,
443
- data() {
444
- if (!snap.exists) return void 0;
445
- const raw = snap.data() ?? {};
446
- const masked = {};
447
- for (const field of fieldMask) {
448
- if (field in raw) {
449
- masked[field] = raw[field];
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
- return masked;
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 chunks;
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, auth: firebaseAuth } = options;
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
- throw new Error(`model '${name}' is already registered`);
672
+ console.warn(`[FlareQuery] model '${name}' is already registered \u2014 skipping duplicate registration`);
473
673
  }
474
674
  models.set(name, definition);
475
675
  },
476
- async execute(query, ctx) {
477
- const queryNode = parseQuery(query);
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.cors) {
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 query = extractQuery(req.body);
518
- if (query === null) {
519
- res.status(400).json({ error: "request body must contain a 'query' string field" });
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
- const response = await app.execute(query, ctx);
524
- res.status(200).json(response);
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.cors) {
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 query = extractQuery(req.body);
543
- if (query === null) {
544
- res.status(400).json({ error: "request body must contain a 'query' string field" });
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
- const response = await app.execute(query, ctx);
549
- res.status(200).json(response);
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
  });