@open-xamu-co/firebase-nuxt 2.0.0-next.1 → 2.0.0-next.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  Firebase nuxt
2
2
 
3
+ # [2.0.0-next.2](https://github.com/xamu-co/firebase-nuxt/compare/v2.0.0-next.1...v2.0.0-next.2) (2026-01-15)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * prevent no content on head responses ([a16a768](https://github.com/xamu-co/firebase-nuxt/commit/a16a7682d3fb322bafda64177f5d57541069beac))
9
+
10
+
11
+ ### Features
12
+
13
+ * accept head & options request ([2822f2f](https://github.com/xamu-co/firebase-nuxt/commit/2822f2ffeb27cc5b6034cb4fe131ae20ae0bde99))
14
+
15
+
16
+ ### BREAKING CHANGES
17
+
18
+ * less args at resolveServerDocumentRefs
19
+
3
20
  # [2.0.0-next.1](https://github.com/xamu-co/firebase-nuxt/compare/v1.2.0-next.2...v2.0.0-next.1) (2026-01-13)
4
21
 
5
22
 
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "1.2.0-next.2",
7
+ "version": "2.0.0-next.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -84,37 +84,40 @@ const module$1 = defineNuxtModule({
84
84
  middleware: true,
85
85
  handler: resolve(runtimePath, "server/middleware/1.context")
86
86
  });
87
- if (moduleOptions.media) {
88
- addServerHandler({
89
- method: "get",
90
- route: "/api/media/**:path",
91
- handler: resolve(runtimePath, "server/api/media.get")
92
- });
93
- }
94
- if (moduleOptions.readCollection) {
95
- addServerHandler({
96
- method: "get",
97
- route: "/api/all/:collectionId",
98
- handler: resolve(runtimePath, "server/api/all-collection.get")
99
- });
100
- addServerHandler({
101
- method: "get",
102
- route: "/api/all/:collectionId/:documentId",
103
- handler: resolve(runtimePath, "server/api/all-collection-document.get")
104
- });
105
- }
106
- if (moduleOptions.readInstanceCollection) {
107
- addServerHandler({
108
- method: "get",
109
- route: "/api/instance/all/:collectionId",
110
- handler: resolve(runtimePath, "server/api/all-collection.get")
111
- });
112
- addServerHandler({
113
- method: "get",
114
- route: "/api/instance/all/:collectionId/:documentId",
115
- handler: resolve(runtimePath, "server/api/all-collection-document.get")
116
- });
117
- }
87
+ const methods = ["get", "head", "options"];
88
+ methods.forEach((method) => {
89
+ if (moduleOptions.media) {
90
+ addServerHandler({
91
+ method,
92
+ route: "/api/media/**:path",
93
+ handler: resolve(runtimePath, "server/api/media")
94
+ });
95
+ }
96
+ if (moduleOptions.readCollection) {
97
+ addServerHandler({
98
+ method,
99
+ route: "/api/all/:collectionId",
100
+ handler: resolve(runtimePath, "server/api/all-collection")
101
+ });
102
+ addServerHandler({
103
+ method,
104
+ route: "/api/all/:collectionId/:documentId",
105
+ handler: resolve(runtimePath, "server/api/all-collection-document")
106
+ });
107
+ }
108
+ if (moduleOptions.readInstanceCollection) {
109
+ addServerHandler({
110
+ method,
111
+ route: "/api/instance/all/:collectionId",
112
+ handler: resolve(runtimePath, "server/api/all-collection")
113
+ });
114
+ addServerHandler({
115
+ method,
116
+ route: "/api/instance/all/:collectionId/:documentId",
117
+ handler: resolve(runtimePath, "server/api/all-collection-document")
118
+ });
119
+ }
120
+ });
118
121
  },
119
122
  moduleDependencies() {
120
123
  const { resolve } = createResolver(import.meta.url);
@@ -1,4 +1,5 @@
1
- import type { LogData } from "../../../functions/types/index.js";
1
+ import type { DocumentReference } from "firebase/firestore";
2
+ import type { LogData, OffenderData } from "../../../functions/types/index.js";
2
3
  import type { FromData } from "./base.js";
3
4
  import type { GetSharedRef } from "./user.js";
4
5
  /** @output Log */
@@ -7,3 +8,11 @@ export interface Log extends FromData<LogData> {
7
8
  /** @input Omit automation */
8
9
  export interface LogRef extends GetSharedRef<Log> {
9
10
  }
11
+ /** @output Offender */
12
+ export interface Offender extends FromData<OffenderData> {
13
+ lastLog?: Log;
14
+ }
15
+ /** @input Omit automation */
16
+ export interface OffenderRef extends GetSharedRef<Offender> {
17
+ lastLogRef?: DocumentReference<LogData>;
18
+ }
@@ -9,7 +9,7 @@ import type { Instance, SharedDocument } from "./instance.js";
9
9
  * Removed properties are not required or are part of automation
10
10
  */
11
11
  export type GetSharedRef<T extends SharedDocument, O extends keyof T = never> = {
12
- [K in keyof FromData<Omit<T, "id" | O>> as K extends `${string}At` ? never : K]: FromData<Omit<T, "id" | O>>[K];
12
+ [K in keyof FromData<Omit<T, O>> as K extends `${string}At` ? never : K]: FromData<Omit<T, O>>[K];
13
13
  } & {
14
14
  createdByRef?: DocumentReference | FieldValue;
15
15
  updatedByRef?: DocumentReference | FieldValue;
@@ -1,11 +1,11 @@
1
1
  import { DocumentReference } from "firebase/firestore";
2
2
  import type { iNodeFnResponseStream } from "@open-xamu-co/ui-common-types";
3
- import type { GetRef, SharedDocument, FirebaseDocument, iSnapshotConfig } from "../../client/types/index.js";
3
+ import type { GetRef, SharedDocument, FirebaseDocument, iSnapshotConfig, FromData } from "../../client/types/index.js";
4
4
  interface iUseDocumentOptions extends iSnapshotConfig {
5
5
  omitLoggings?: boolean;
6
6
  }
7
7
  /** Creates document with the given values */
8
- export declare function useDocumentCreate<Vgr extends GetRef<V>, V extends FirebaseDocument = FirebaseDocument>(collectionPath: string, partialRef: Vgr, createdCallback?: (ref: DocumentReference<Vgr, V>) => Promise<void> | void, { omitLoggings, ...config }?: iUseDocumentOptions): Promise<iNodeFnResponseStream<V>>;
8
+ export declare function useDocumentCreate<Vgr extends GetRef<SharedDocument>, V extends FromData<Vgr> = FromData<Vgr>>(collectionPath: string, partialRef: Vgr, createdCallback?: (ref: DocumentReference<Vgr, V>) => Promise<void> | void, { omitLoggings, ...config }?: iUseDocumentOptions): Promise<iNodeFnResponseStream<V>>;
9
9
  /**
10
10
  * Updates a given document in Firestore.
11
11
  *
@@ -13,9 +13,9 @@ export declare function useDocumentCreate<Vgr extends GetRef<V>, V extends Fireb
13
13
  * @param partialRef - The partial data to update the document with.
14
14
  * @returns A boolean promise.
15
15
  */
16
- export declare function useDocumentUpdate<Vgr extends GetRef<V>, V extends FirebaseDocument = FirebaseDocument>(node: SharedDocument, middleRef?: Partial<Vgr>, { omitLoggings, ...config }?: iUseDocumentOptions): Promise<iNodeFnResponseStream<V>>;
16
+ export declare function useDocumentUpdate<Vgr extends GetRef<SharedDocument>, V extends FromData<Vgr> = FromData<Vgr>>(node: SharedDocument, middleRef?: Partial<Vgr>, { omitLoggings, ...config }?: iUseDocumentOptions): Promise<iNodeFnResponseStream<V>>;
17
17
  /** Clones given document */
18
- export declare function useDocumentClone<Vgr extends GetRef<V>, V extends FirebaseDocument = FirebaseDocument>(node: SharedDocument, middleRef?: Partial<Vgr>, { omitLoggings, ...config }?: iUseDocumentOptions): Promise<iNodeFnResponseStream<V>>;
18
+ export declare function useDocumentClone<Vgr extends GetRef<SharedDocument>, V extends FromData<Vgr> = FromData<Vgr>>(node: SharedDocument, middleRef?: Partial<Vgr>, { omitLoggings, ...config }?: iUseDocumentOptions): Promise<iNodeFnResponseStream<V>>;
19
19
  /** Deletes given document */
20
20
  export declare function useDocumentDelete<T extends FirebaseDocument = FirebaseDocument>(node: SharedDocument, { omitLoggings }?: iUseDocumentOptions): Promise<iNodeFnResponseStream<T>>;
21
21
  export {};
@@ -30,6 +30,12 @@ export interface RootData extends InstanceData {
30
30
  /** @example "Xamu" */
31
31
  poweredBy?: string;
32
32
  }
33
+ export interface InstanceDataConfig extends FirebaseData {
34
+ /**
35
+ * When tenants are enabled, domains are required
36
+ */
37
+ domains?: string[];
38
+ }
33
39
  /**
34
40
  * App instance
35
41
  *
@@ -46,13 +52,7 @@ export interface InstanceData extends SharedData {
46
52
  * @automated
47
53
  */
48
54
  url?: string;
49
- config?: {
50
- [key: string]: any;
51
- /**
52
- * When tenants are enabled, domains are required
53
- */
54
- domains?: string[];
55
- };
55
+ config?: InstanceDataConfig;
56
56
  }
57
57
  /**
58
58
  * Firebase log
@@ -1,3 +1,4 @@
1
+ import type { DocumentReference } from "firebase-admin/firestore";
1
2
  import type { FirebaseData } from "./base.js";
2
3
  /** General log entity */
3
4
  export interface LogData extends FirebaseData {
@@ -13,3 +14,28 @@ export interface LogData extends FirebaseData {
13
14
  */
14
15
  internal?: boolean;
15
16
  }
17
+ /**
18
+ * Offender entity
19
+ *
20
+ * Keep track of abbussive requests
21
+ */
22
+ export interface OffenderData extends FirebaseData {
23
+ /** IP address */
24
+ ip?: string;
25
+ /** ISO country codes */
26
+ countries?: string[];
27
+ /** User agents */
28
+ userAgents?: string[];
29
+ /** Preferred languages */
30
+ languages?: string[];
31
+ /**
32
+ * Number of hits
33
+ * @automated
34
+ */
35
+ hits?: number;
36
+ /**
37
+ * Log reference
38
+ * @automated
39
+ */
40
+ lastLogRef?: DocumentReference<LogData>;
41
+ }
@@ -1,3 +1,7 @@
1
1
  import { DocumentReference, type Firestore } from "firebase-admin/firestore";
2
2
  import type { tLogger } from "@open-xamu-co/ui-common-types";
3
3
  export declare function makeFunctionsLogger(at: DocumentReference | Firestore, authorRef?: DocumentReference, metadata?: Record<string, any>): tLogger;
4
+ /**
5
+ * Log offending requests sources
6
+ */
7
+ export declare function offenderLogger(at: DocumentReference | Firestore, lastLogRef: DocumentReference, metadata?: any): void;
@@ -1,4 +1,8 @@
1
- import { DocumentReference } from "firebase-admin/firestore";
1
+ import {
2
+ DocumentReference,
3
+ FieldValue
4
+ } from "firebase-admin/firestore";
5
+ import acceptLanguageParser from "accept-language-parser";
2
6
  import { getLog } from "./logs.js";
3
7
  export function makeFunctionsLogger(at, authorRef, metadata = {}) {
4
8
  return function(...args) {
@@ -15,3 +19,22 @@ export function makeFunctionsLogger(at, authorRef, metadata = {}) {
15
19
  }
16
20
  };
17
21
  }
22
+ export function offenderLogger(at, lastLogRef, metadata) {
23
+ if (!metadata || typeof metadata !== "object") return;
24
+ const headers = metadata.headers || {};
25
+ const ip = headers["cf-connecting-ip"] || headers["x-fah-client-ip"] || headers["ip"];
26
+ const userAgent = headers["from"] || headers["user-agent"];
27
+ const country = headers["cf-ipcountry"];
28
+ const [preferredLanguage] = acceptLanguageParser.parse(headers["accept-language"]);
29
+ if (!ip) return;
30
+ const offendersRef = at.collection("offenders");
31
+ const offenderDoc = offendersRef.doc(ip);
32
+ const offenderData = { ip, hits: FieldValue.increment(1), lastLogRef };
33
+ if (country) offenderData.countries = FieldValue.arrayUnion(country);
34
+ if (userAgent) offenderData.userAgents = FieldValue.arrayUnion(userAgent);
35
+ if (preferredLanguage) {
36
+ const { code, region } = preferredLanguage;
37
+ offenderData.languages = FieldValue.arrayUnion(`${code}-${region}`);
38
+ }
39
+ offenderDoc.set(offenderData, { merge: true });
40
+ }
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @auth guest
5
5
  */
6
- declare const _default: import("../types/index.js").CachedEventHandler<import("h3").EventHandlerRequest, Promise<void | FirebaseFirestore.DocumentData>>;
6
+ declare const _default: import("../types/index.js").CachedEventHandler<import("h3").EventHandlerRequest, Promise<void | FirebaseFirestore.DocumentData | "Ok">>;
7
7
  export default _default;
@@ -0,0 +1,75 @@
1
+ import {
2
+ createError,
3
+ getRouterParam,
4
+ isError,
5
+ sendNoContent,
6
+ setResponseHeaders,
7
+ setResponseStatus
8
+ } from "h3";
9
+ import { defineConditionallyCachedEventHandler } from "../utils/cache.js";
10
+ import { debugFirebaseServer, resolveServerDocumentRefs } from "../utils/firestore.js";
11
+ import { apiLogger, getServerFirebase } from "../utils/firebase.js";
12
+ import { readCollection, readInstanceCollection } from "#internal/firebase-nuxt";
13
+ export default defineConditionallyCachedEventHandler(async (event) => {
14
+ const { firebaseFirestore } = getServerFirebase("api:all:[collectionId]:[documentId]");
15
+ const { currentInstanceRef } = event.context;
16
+ const Allow = "GET,HEAD";
17
+ try {
18
+ setResponseHeaders(event, {
19
+ Allow,
20
+ "Access-Control-Allow-Methods": Allow,
21
+ "Content-Type": "application/json"
22
+ });
23
+ if (!["GET", "HEAD", "OPTIONS"].includes(event.method?.toUpperCase())) {
24
+ throw createError({ statusCode: 405, statusMessage: "Unsupported method" });
25
+ } else if (event.method?.toUpperCase() === "OPTIONS") {
26
+ return sendNoContent(event);
27
+ }
28
+ const collectionId = getRouterParam(event, "collectionId");
29
+ const documentId = getRouterParam(event, "documentId");
30
+ debugFirebaseServer(event, "api:all:[collectionId]:[documentId]", collectionId, documentId);
31
+ if (!collectionId || !documentId) {
32
+ throw createError({
33
+ statusCode: 400,
34
+ statusMessage: `collectionId & documentId are required`
35
+ });
36
+ }
37
+ let collectionRef = firebaseFirestore.collection(collectionId);
38
+ if (event.path.startsWith("/api/instance/all")) {
39
+ if (!currentInstanceRef) {
40
+ throw createError({ statusCode: 401, statusMessage: "Missing instance" });
41
+ } else if (!readInstanceCollection(collectionId, event.context)) {
42
+ throw createError({
43
+ statusCode: 401,
44
+ statusMessage: `Can't get "instance/${collectionId}" document`
45
+ });
46
+ }
47
+ collectionRef = currentInstanceRef.collection(collectionId);
48
+ } else if (!readCollection(collectionId, event.context)) {
49
+ throw createError({
50
+ statusCode: 401,
51
+ statusMessage: `Can't get "${collectionId}" document`
52
+ });
53
+ }
54
+ const documentsRef = collectionRef.doc(documentId);
55
+ const documentSnapshot = await documentsRef.get();
56
+ if (!documentSnapshot?.exists) {
57
+ let statusMessage = `No "${collectionId}" document matched`;
58
+ if (documentSnapshot?.ref.path) {
59
+ statusMessage = `${statusMessage} for ${documentSnapshot.ref.path}`;
60
+ }
61
+ throw createError({ statusCode: 404, statusMessage });
62
+ }
63
+ if (event.method?.toUpperCase() === "HEAD") {
64
+ setResponseStatus(event, 200);
65
+ return "Ok";
66
+ }
67
+ return resolveServerDocumentRefs(event, documentSnapshot);
68
+ } catch (err) {
69
+ if (isError(err)) {
70
+ apiLogger(event, "api:all:[collectionId]:[documentId]", err.message, err);
71
+ return setResponseStatus(event, err.statusCode || 500, err.statusMessage);
72
+ }
73
+ throw err;
74
+ }
75
+ });
@@ -4,5 +4,5 @@
4
4
  * @auth guest
5
5
  * @order createdAt
6
6
  */
7
- declare const _default: import("../types/index.js").CachedEventHandler<import("h3").EventHandlerRequest, Promise<void | import("@open-xamu-co/ui-common-types").iPageEdge<FirebaseFirestore.DocumentData, string>[] | import("@open-xamu-co/ui-common-types").iPage<FirebaseFirestore.DocumentData, string>>>;
7
+ declare const _default: import("../types/index.js").CachedEventHandler<import("h3").EventHandlerRequest, Promise<void | import("@open-xamu-co/ui-common-types").iPageEdge<FirebaseFirestore.DocumentData, string>[] | import("@open-xamu-co/ui-common-types").iPage<FirebaseFirestore.DocumentData, string> | "Ok">>;
8
8
  export default _default;
@@ -0,0 +1,85 @@
1
+ import { FieldPath } from "firebase-admin/firestore";
2
+ import {
3
+ createError,
4
+ getQuery,
5
+ getRouterParam,
6
+ isError,
7
+ sendNoContent,
8
+ setResponseHeaders,
9
+ setResponseStatus
10
+ } from "h3";
11
+ import { defineConditionallyCachedEventHandler } from "../utils/cache.js";
12
+ import { getBoolean } from "../utils/guards.js";
13
+ import { getDocumentId } from "../../client/utils/resolver.js";
14
+ import {
15
+ debugFirebaseServer,
16
+ getEdgesPage,
17
+ getOrderedQuery,
18
+ getQueryAsEdges
19
+ } from "../utils/firestore.js";
20
+ import { apiLogger, getServerFirebase } from "../utils/firebase.js";
21
+ import { readCollection, readInstanceCollection } from "#internal/firebase-nuxt";
22
+ export default defineConditionallyCachedEventHandler(async (event) => {
23
+ const fieldDocument = FieldPath.documentId();
24
+ const { firebaseFirestore } = getServerFirebase("api:all:[collectionId]");
25
+ const { currentInstanceRef } = event.context;
26
+ const Allow = "GET,HEAD";
27
+ try {
28
+ setResponseHeaders(event, {
29
+ Allow,
30
+ "Access-Control-Allow-Methods": Allow,
31
+ "Content-Type": "application/json"
32
+ });
33
+ if (!["GET", "HEAD", "OPTIONS"].includes(event.method?.toUpperCase())) {
34
+ throw createError({ statusCode: 405, statusMessage: "Unsupported method" });
35
+ } else if (event.method?.toUpperCase() === "OPTIONS") {
36
+ return sendNoContent(event);
37
+ }
38
+ const params = getQuery(event);
39
+ const page = getBoolean(params.page);
40
+ const collectionId = getRouterParam(event, "collectionId");
41
+ debugFirebaseServer(event, "api:all:[collectionId]", collectionId);
42
+ if (!collectionId) {
43
+ throw createError({ statusCode: 400, statusMessage: `collectionId is required` });
44
+ }
45
+ let query = firebaseFirestore.collection(collectionId);
46
+ if (event.path.startsWith("/api/instance/all")) {
47
+ if (!currentInstanceRef) {
48
+ throw createError({ statusCode: 401, statusMessage: "Missing instance" });
49
+ } else if (!readInstanceCollection(collectionId, event.context)) {
50
+ throw createError({
51
+ statusCode: 401,
52
+ statusMessage: `Can't list "instance/${collectionId}"`
53
+ });
54
+ }
55
+ query = currentInstanceRef.collection(collectionId);
56
+ } else if (!readCollection(collectionId, event.context)) {
57
+ throw createError({
58
+ statusCode: 401,
59
+ statusMessage: `Can't list "${collectionId}"`
60
+ });
61
+ }
62
+ if (event.method?.toUpperCase() === "HEAD") {
63
+ setResponseStatus(event, 200);
64
+ return "Ok";
65
+ }
66
+ if (params.include) {
67
+ let include = Array.isArray(params.include) ? params.include : [params.include];
68
+ include = include.filter((uid) => uid && !getBoolean(uid)).map(getDocumentId);
69
+ debugFirebaseServer(event, "api:all:[collectionId]:filtered", include);
70
+ if (!include.length) return [];
71
+ query = query.orderBy(fieldDocument).where(fieldDocument, "in", include);
72
+ return getQueryAsEdges(event, query);
73
+ }
74
+ query = getOrderedQuery(event, query);
75
+ if (page) return getEdgesPage(event, query);
76
+ const first = Math.min(Number(params.first) || 10, 100);
77
+ return getQueryAsEdges(event, query.limit(first));
78
+ } catch (err) {
79
+ if (isError(err)) {
80
+ apiLogger(event, "api:all:[collectionId]", err.message, err);
81
+ return setResponseStatus(event, err.statusCode || 500, err.statusMessage);
82
+ }
83
+ throw err;
84
+ }
85
+ });
@@ -4,5 +4,5 @@
4
4
  * Buffer check because of nitro issue:
5
5
  * @see https://github.com/unjs/nitro/issues/1894
6
6
  */
7
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | Buffer<ArrayBuffer>>>;
7
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | Buffer<ArrayBuffer> | "Ok">>;
8
8
  export default _default;
@@ -7,7 +7,10 @@ import {
7
7
  getRouterParam,
8
8
  isError,
9
9
  sendError,
10
- setHeaders
10
+ sendNoContent,
11
+ setHeaders,
12
+ setResponseHeaders,
13
+ setResponseStatus
11
14
  } from "h3";
12
15
  import { storageBucket } from "../utils/environment.js";
13
16
  import { debugFirebaseServer } from "../utils/firestore.js";
@@ -52,6 +55,7 @@ const cachedBufferHandler = defineCachedFunction(
52
55
  const [exists] = await file.exists();
53
56
  const headers = { "Content-Type": "image/webp" };
54
57
  if (exists) {
58
+ if (event.method?.toUpperCase() === "HEAD") return { headers };
55
59
  const [buffer] = await file.download();
56
60
  return { buffer, headers };
57
61
  }
@@ -83,16 +87,28 @@ const cachedBufferHandler = defineCachedFunction(
83
87
  maxAge,
84
88
  getKey(event, path) {
85
89
  const [baseAndExtension] = path.split("?");
86
- return createHash("sha256").update(baseAndExtension).digest("hex");
90
+ const hash = createHash("sha256").update(baseAndExtension).digest("hex");
91
+ return `${hash}:${event.method}`;
87
92
  }
88
93
  }
89
94
  );
90
95
  export default defineEventHandler(async (event) => {
91
96
  const path = getRouterParam(event, "path") || "";
97
+ const Allow = "GET,HEAD";
92
98
  try {
93
- const { buffer, headers, error } = await cachedBufferHandler(event, path);
94
- if (headers) setHeaders(event, headers);
99
+ setResponseHeaders(event, { Allow, "Access-Control-Allow-Methods": Allow });
100
+ if (!["GET", "HEAD", "OPTIONS"].includes(event.method?.toUpperCase())) {
101
+ throw createError({ statusCode: 405, statusMessage: "Unsupported method" });
102
+ } else if (event.method?.toUpperCase() === "OPTIONS") {
103
+ return sendNoContent(event);
104
+ }
105
+ const { buffer, headers = {}, error } = await cachedBufferHandler(event, path);
106
+ setHeaders(event, headers);
95
107
  if (error || !buffer) {
108
+ if (!error && event.method?.toUpperCase() === "HEAD") {
109
+ setResponseStatus(event, 200);
110
+ return "Ok";
111
+ }
96
112
  const err = error || createError({
97
113
  statusCode: 500,
98
114
  statusMessage: `Something went wrong while trying to get file with path: "${path}"`
@@ -18,20 +18,7 @@ export default defineEventHandler(async (event) => {
18
18
  const { firebaseFirestore } = getServerFirebase("api:middleware:context");
19
19
  const headers = getRequestHeaders(event);
20
20
  const corsHeaders = ["Origin", "Referer", "User-Agent"].join(", ");
21
- if (!event.path.startsWith("/api")) {
22
- setResponseHeaders(event, {
23
- "Access-Control-Allow-Methods": "GET,HEAD",
24
- Vary: "Host, Origin"
25
- });
26
- if (event.method === "OPTIONS") {
27
- setResponseHeaders(event, {
28
- "Access-Control-Allow-Headers": corsHeaders,
29
- "Access-Control-Expose-Headers": corsHeaders
30
- });
31
- setResponseStatus(event, 204, "No Content");
32
- }
33
- return;
34
- }
21
+ if (!event.path.startsWith("/api")) return;
35
22
  const {
36
23
  host = "",
37
24
  "x-forwarded-host": forwardedHost = host,
@@ -57,18 +44,19 @@ export default defineEventHandler(async (event) => {
57
44
  "Host",
58
45
  "Xamu-Context-Source"
59
46
  ].join(", ");
47
+ const Allow = "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS";
60
48
  setResponseHeaders(event, {
61
- "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
49
+ Allow,
50
+ "Access-Control-Allow-Methods": Allow,
62
51
  "Access-Control-Allow-Credentials": "true",
63
52
  "Access-Control-Allow-Origin": corsOrigin,
64
53
  Vary: "Host, Origin"
65
54
  });
66
- if (event.method === "OPTIONS") {
55
+ if (event.method?.toUpperCase() === "OPTIONS") {
67
56
  setResponseHeaders(event, {
68
57
  "Access-Control-Allow-Headers": corsHeadersAccept,
69
58
  "Access-Control-Expose-Headers": corsHeadersExpose
70
59
  });
71
- setResponseStatus(event, 204, "No Content");
72
60
  }
73
61
  const authorization = getRequestHeader(event, "X-Forwarded-Authorization") || getRequestHeader(event, "Authorization");
74
62
  if (!authorization) return;
@@ -5,6 +5,8 @@ interface CachedEventHandlerOptions<T extends EventHandlerRequest = EventHandler
5
5
  getKey?: (...args: [CachedH3Event<T>]) => string | Promise<string>;
6
6
  /** Partition cache by instance */
7
7
  instanceOnly?: boolean;
8
+ /** Partition cache by method */
9
+ methodOnly?: boolean;
8
10
  }
9
11
  /**
10
12
  * Conditionally cache event data.
@@ -17,5 +19,5 @@ interface CachedEventHandlerOptions<T extends EventHandlerRequest = EventHandler
17
19
  * @param options optional key generator and instanceOnly flag
18
20
  * @returns event handler
19
21
  */
20
- export declare const defineConditionallyCachedEventHandler: <T extends EventHandlerRequest, D extends EventHandlerResponse = EventHandlerResponse>(handler: CachedEventHandler<T, D>, { getKey, instanceOnly }?: CachedEventHandlerOptions<T>) => CachedEventHandler<T, D>;
22
+ export declare const defineConditionallyCachedEventHandler: <T extends EventHandlerRequest, D extends EventHandlerResponse = EventHandlerResponse>(handler: CachedEventHandler<T, D>, { getKey, instanceOnly, methodOnly }?: CachedEventHandlerOptions<T>) => CachedEventHandler<T, D>;
21
23
  export {};
@@ -2,16 +2,17 @@ import { defineEventHandler } from "h3";
2
2
  import { defineCachedEventHandler } from "nitropack/runtime";
3
3
  import { debugNitro } from "../utils/environment.js";
4
4
  import { sudo } from "#internal/firebase-nuxt";
5
- export const defineConditionallyCachedEventHandler = (handler, { getKey, instanceOnly = true } = {}) => {
5
+ export const defineConditionallyCachedEventHandler = (handler, { getKey, instanceOnly = true, methodOnly = true } = {}) => {
6
6
  const cachedHandler = defineCachedEventHandler(handler, {
7
7
  maxAge: 30,
8
8
  // 30 seconds
9
- getKey: instanceOnly ? (event) => {
9
+ getKey(event) {
10
10
  const { currentInstanceHost } = event.context;
11
- const key = getKey?.(event) || event.path;
12
- if (currentInstanceHost) return `${currentInstanceHost}:${key}`;
11
+ let key = getKey?.(event) || event.path;
12
+ if (instanceOnly && currentInstanceHost) key += `:${currentInstanceHost}`;
13
+ if (methodOnly) key += `:${event.method}`;
13
14
  return key;
14
- } : getKey
15
+ }
15
16
  });
16
17
  return defineEventHandler(async (event) => {
17
18
  if (sudo(event.context) || debugNitro.value()) return handler(event);
@@ -9,7 +9,7 @@ export declare function debugFirebaseServer<T extends EventHandlerRequest>(event
9
9
  /**
10
10
  * This one is used on api endpoints
11
11
  */
12
- export declare function resolveServerDocumentRefs<T extends PseudoNode, R extends FromData<T> = FromData<T>>(event: H3Event, snapshot?: DocumentSnapshot<T, R>, collection?: string, withAuth?: boolean): Promise<R | undefined>;
12
+ export declare function resolveServerDocumentRefs<T extends PseudoNode, R extends FromData<T> = FromData<T>>(event: H3Event, snapshot?: DocumentSnapshot<T, R>, withAuth?: boolean): Promise<R | undefined> | undefined;
13
13
  /**
14
14
  * Resolve general refs
15
15
  */
@@ -1,4 +1,4 @@
1
- import { getRequestURL, createError, getQuery } from "h3";
1
+ import { getRequestURL, getQuery } from "h3";
2
2
  import { useEvent } from "nitropack/runtime";
3
3
  import { getBoolean, isNumberOrString } from "../utils/guards.js";
4
4
  import { makeResolveRefs } from "../../client/utils/resolver.js";
@@ -17,16 +17,13 @@ export function debugFirebaseServer(event, mss, ...args) {
17
17
  console.groupEnd();
18
18
  }
19
19
  }
20
- export function resolveServerDocumentRefs(event, snapshot, collection = "documents", withAuth) {
21
- if (!snapshot?.exists) {
22
- let statusMessage = `No "${collection}" matched`;
23
- if (snapshot?.ref.path) statusMessage = `${statusMessage} for ${snapshot.ref.path}`;
24
- throw createError({ statusCode: 404, statusMessage });
20
+ export function resolveServerDocumentRefs(event, snapshot, withAuth) {
21
+ if (snapshot?.exists) {
22
+ const params = getQuery(event);
23
+ const level = Array.isArray(params.level) || !params.level ? 0 : Number(params.level);
24
+ const omit = Array.isArray(params.omit) ? params.omit : [params.omit];
25
+ return resolveServerRefs(snapshot, { level, omit }, withAuth);
25
26
  }
26
- const params = getQuery(event);
27
- const level = Array.isArray(params.level) || !params.level ? 0 : Number(params.level);
28
- const omit = Array.isArray(params.omit) ? params.omit : [params.omit];
29
- return resolveServerRefs(snapshot, { level, omit }, withAuth);
30
27
  }
31
28
  export async function resolveServerRefs(snapshot, config = {}, withAuth) {
32
29
  const event = useEvent();
@@ -25,8 +25,8 @@ export const getInstance = defineCachedFunction(
25
25
  }
26
26
  }
27
27
  const millis = snapshot.data()?.createdAt?.toMillis();
28
- const instance = await resolveServerDocumentRefs(event, snapshot, "instances", false) || {};
29
- if (!instance.id || !millis) {
28
+ const instance = await resolveServerDocumentRefs(event, snapshot, false);
29
+ if (!instance?.id || !millis) {
30
30
  throw createError({
31
31
  statusCode: 502,
32
32
  statusMessage: `Invalid app instance for ${host}`,
@@ -59,7 +59,7 @@ export const getRootInstance = defineCachedFunction(
59
59
  const instancesRef = firebaseFirestore.collection("instances");
60
60
  debugFirebaseServer(event, "middleware:getRootInstance");
61
61
  const snapshot = await instancesRef.doc(rootInstanceId).get();
62
- return resolveServerDocumentRefs(event, snapshot, "instances", false) || {};
62
+ return resolveServerDocumentRefs(event, snapshot, false) || {};
63
63
  },
64
64
  {
65
65
  name: "getRootInstance",
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@open-xamu-co/firebase-nuxt",
3
- "version": "2.0.0-next.1",
3
+ "version": "2.0.0-next.2",
4
+ "stableVersion": "1.1.0",
4
5
  "description": "Nuxt 3 module for the xamu project",
5
6
  "author": "@xamu-co",
6
7
  "type": "module",
@@ -88,6 +89,7 @@
88
89
  "@open-xamu-co/ui-common-helpers": "^4.0.0-next.2",
89
90
  "@open-xamu-co/ui-nuxt": "^4.0.0-next.6",
90
91
  "@pinia/nuxt": "^0.11.0",
92
+ "accept-language-parser": "^1.5.0",
91
93
  "firebase": "^11.0.2",
92
94
  "firebase-admin": "^13.6.0",
93
95
  "firebase-functions": "^7.0.3",
@@ -160,6 +162,5 @@
160
162
  },
161
163
  "browserslist": [
162
164
  "defaults"
163
- ],
164
- "stableVersion": "1.1.0"
165
+ ]
165
166
  }
@@ -1,56 +0,0 @@
1
- import { createError, getRouterParam, isError, setResponseStatus } from "h3";
2
- import { defineConditionallyCachedEventHandler } from "../utils/cache.js";
3
- import { debugFirebaseServer, resolveServerDocumentRefs } from "../utils/firestore.js";
4
- import { apiLogger, getServerFirebase } from "../utils/firebase.js";
5
- import { readCollection, readInstanceCollection } from "#internal/firebase-nuxt";
6
- export default defineConditionallyCachedEventHandler(
7
- async (event) => {
8
- const { firebaseFirestore } = getServerFirebase("api:all:[collectionId]:[documentId]");
9
- const { currentInstanceRef } = event.context;
10
- try {
11
- const collectionId = getRouterParam(event, "collectionId");
12
- const documentId = getRouterParam(event, "documentId");
13
- debugFirebaseServer(
14
- event,
15
- "api:all:[collectionId]:[documentId]",
16
- collectionId,
17
- documentId
18
- );
19
- if (!collectionId || !documentId) {
20
- throw createError({
21
- statusCode: 400,
22
- statusMessage: `collectionId & documentId are required`
23
- });
24
- }
25
- let collectionRef = firebaseFirestore.collection(collectionId);
26
- if (event.path.startsWith("/api/instance/all")) {
27
- if (!currentInstanceRef) {
28
- throw createError({ statusCode: 401, statusMessage: "Missing instance" });
29
- } else if (!readInstanceCollection(collectionId, event.context)) {
30
- throw createError({
31
- statusCode: 401,
32
- statusMessage: `Can't get "instance/${collectionId}" document`
33
- });
34
- }
35
- collectionRef = currentInstanceRef.collection(collectionId);
36
- } else if (!readCollection(collectionId, event.context)) {
37
- throw createError({
38
- statusCode: 401,
39
- statusMessage: `Can't get "${collectionId}" document`
40
- });
41
- }
42
- const documentsRef = collectionRef.doc(documentId);
43
- const documentSnapshot = await documentsRef.get();
44
- return resolveServerDocumentRefs(event, documentSnapshot, collectionId);
45
- } catch (err) {
46
- if (isError(err)) {
47
- apiLogger(event, "api:all:[collectionId]:[documentId]", err.message, err);
48
- return setResponseStatus(event, err.statusCode || 500, err.statusMessage);
49
- }
50
- throw err;
51
- }
52
- },
53
- {
54
- instanceOnly: false
55
- }
56
- );
@@ -1,67 +0,0 @@
1
- import { FieldPath } from "firebase-admin/firestore";
2
- import { createError, getQuery, getRouterParam, isError, setResponseStatus } from "h3";
3
- import { defineConditionallyCachedEventHandler } from "../utils/cache.js";
4
- import { getBoolean } from "../utils/guards.js";
5
- import { getDocumentId } from "../../client/utils/resolver.js";
6
- import {
7
- debugFirebaseServer,
8
- getEdgesPage,
9
- getOrderedQuery,
10
- getQueryAsEdges
11
- } from "../utils/firestore.js";
12
- import { apiLogger, getServerFirebase } from "../utils/firebase.js";
13
- import { readCollection, readInstanceCollection } from "#internal/firebase-nuxt";
14
- export default defineConditionallyCachedEventHandler(
15
- async (event) => {
16
- const fieldDocument = FieldPath.documentId();
17
- const { firebaseFirestore } = getServerFirebase("api:all:[collectionId]");
18
- const { currentInstanceRef } = event.context;
19
- try {
20
- const params = getQuery(event);
21
- const page = getBoolean(params.page);
22
- const collectionId = getRouterParam(event, "collectionId");
23
- debugFirebaseServer(event, "api:all:[collectionId]", collectionId);
24
- if (!collectionId) {
25
- throw createError({ statusCode: 400, statusMessage: `collectionId is required` });
26
- }
27
- let query = firebaseFirestore.collection(collectionId);
28
- if (event.path.startsWith("/api/instance/all")) {
29
- if (!currentInstanceRef) {
30
- throw createError({ statusCode: 401, statusMessage: "Missing instance" });
31
- } else if (!readInstanceCollection(collectionId, event.context)) {
32
- throw createError({
33
- statusCode: 401,
34
- statusMessage: `Can't list "instance/${collectionId}"`
35
- });
36
- }
37
- query = currentInstanceRef.collection(collectionId);
38
- } else if (!readCollection(collectionId, event.context)) {
39
- throw createError({
40
- statusCode: 401,
41
- statusMessage: `Can't list "${collectionId}"`
42
- });
43
- }
44
- if (params.include) {
45
- let include = Array.isArray(params.include) ? params.include : [params.include];
46
- include = include.filter((uid) => uid && !getBoolean(uid)).map(getDocumentId);
47
- debugFirebaseServer(event, "api:all:[collectionId]:filtered", include);
48
- if (!include.length) return [];
49
- query = query.orderBy(fieldDocument).where(fieldDocument, "in", include);
50
- return getQueryAsEdges(event, query);
51
- }
52
- query = getOrderedQuery(event, query);
53
- if (page) return getEdgesPage(event, query);
54
- const first = Math.min(Number(params.first) || 10, 100);
55
- return getQueryAsEdges(event, query.limit(first));
56
- } catch (err) {
57
- if (isError(err)) {
58
- apiLogger(event, "api:all:[collectionId]", err.message, err);
59
- return setResponseStatus(event, err.statusCode || 500, err.statusMessage);
60
- }
61
- throw err;
62
- }
63
- },
64
- {
65
- instanceOnly: false
66
- }
67
- );