@progressive-development/pd-provider-firebase-functions 1.3.0 → 1.3.1

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.
@@ -1,52 +1,63 @@
1
- import { getApps, initializeApp, cert } from 'firebase-admin/app';
2
- import { existsSync, readFileSync } from 'fs';
3
- import { getDb } from './firestore.js';
4
-
5
- const LOG_PREFIX = {
6
- info: "i ",
7
- success: "OK",
8
- error: "X ",
9
- warn: "! "
1
+ import { getDb } from "./firestore.js";
2
+ import { cert, getApps, initializeApp } from "firebase-admin/app";
3
+ import { existsSync, readFileSync } from "fs";
4
+ //#region src/script-helpers.ts
5
+ /**
6
+ * Shared helpers for Firebase Admin scripts.
7
+ *
8
+ * Provides common boilerplate: logging, service-account validation,
9
+ * Firebase init, Firestore access, and dry-run handling.
10
+ */
11
+ var LOG_PREFIX = {
12
+ info: "i ",
13
+ success: "OK",
14
+ error: "X ",
15
+ warn: "! "
10
16
  };
17
+ /**
18
+ * Print a prefixed log message to console.
19
+ */
11
20
  function scriptLog(message, type = "info") {
12
- console.log(`${LOG_PREFIX[type]} ${message}`);
21
+ console.log(`${LOG_PREFIX[type]} ${message}`);
13
22
  }
23
+ /**
24
+ * Initialize Firebase Admin SDK if not already initialized.
25
+ */
14
26
  function initFirebaseAdmin(serviceAccountPath) {
15
- if (getApps().length > 0) return;
16
- const serviceAccount = JSON.parse(
17
- readFileSync(serviceAccountPath, "utf-8")
18
- );
19
- initializeApp({
20
- credential: cert(serviceAccount),
21
- databaseURL: `https://${serviceAccount.projectId}.firebaseio.com`
22
- });
27
+ if (getApps().length > 0) return;
28
+ const serviceAccount = JSON.parse(readFileSync(serviceAccountPath, "utf-8"));
29
+ initializeApp({
30
+ credential: cert(serviceAccount),
31
+ databaseURL: `https://${serviceAccount.projectId}.firebaseio.com`
32
+ });
23
33
  }
34
+ /**
35
+ * Set up a script context: validates paths, initializes Firebase,
36
+ * returns a ready-to-use Firestore instance.
37
+ *
38
+ * Prints title, connection info, and dry-run banner.
39
+ * Calls `process.exit(1)` if the service account key is missing.
40
+ */
24
41
  function createScriptContext(config) {
25
- const { title, serviceAccountPath, databaseId, dryRun = false } = config;
26
- console.log(`
27
- ${title}
28
- `);
29
- console.log("-".repeat(50));
30
- if (!existsSync(serviceAccountPath)) {
31
- scriptLog("Service Account Key not found!", "error");
32
- console.log(`
33
- Expected: ${serviceAccountPath}`);
34
- console.log(`
35
- Download from Firebase Console:`);
36
- console.log(
37
- ` Project Settings > Service Accounts > Generate new private key
38
- `
39
- );
40
- process.exit(1);
41
- }
42
- initFirebaseAdmin(serviceAccountPath);
43
- const db = getDb(databaseId);
44
- scriptLog(`Firestore: ${databaseId ?? "(default)"}`);
45
- if (dryRun) {
46
- scriptLog("DRY RUN - No changes will be made", "warn");
47
- }
48
- console.log("-".repeat(50));
49
- return { db, dryRun };
42
+ const { title, serviceAccountPath, databaseId, dryRun = false } = config;
43
+ console.log(`\n${title}\n`);
44
+ console.log("-".repeat(50));
45
+ if (!existsSync(serviceAccountPath)) {
46
+ scriptLog("Service Account Key not found!", "error");
47
+ console.log(`\n Expected: ${serviceAccountPath}`);
48
+ console.log(`\n Download from Firebase Console:`);
49
+ console.log(` Project Settings > Service Accounts > Generate new private key\n`);
50
+ process.exit(1);
51
+ }
52
+ initFirebaseAdmin(serviceAccountPath);
53
+ const db = getDb(databaseId);
54
+ scriptLog(`Firestore: ${databaseId ?? "(default)"}`);
55
+ if (dryRun) scriptLog("DRY RUN - No changes will be made", "warn");
56
+ console.log("-".repeat(50));
57
+ return {
58
+ db,
59
+ dryRun
60
+ };
50
61
  }
51
-
62
+ //#endregion
52
63
  export { createScriptContext, initFirebaseAdmin, scriptLog };
package/dist/trash.js CHANGED
@@ -1,83 +1,116 @@
1
- import { Timestamp } from 'firebase-admin/firestore';
2
- import { getDb } from './firestore.js';
3
- import { NotFoundError } from './errors.js';
4
-
5
- const TRASH_COLLECTION = "_trash";
1
+ import { NotFoundError } from "./errors.js";
2
+ import { getDb } from "./firestore.js";
3
+ import { Timestamp } from "firebase-admin/firestore";
4
+ //#region src/trash.ts
5
+ var TRASH_COLLECTION = "_trash";
6
+ /**
7
+ * Move a document to trash.
8
+ *
9
+ * Atomically:
10
+ * 1. Reads the original document
11
+ * 2. Creates a trash entry with metadata
12
+ * 3. Deletes the original document
13
+ *
14
+ * @param collection - The collection path (e.g., "djRequests")
15
+ * @param documentId - The document ID to trash
16
+ * @param deletedBy - UID of the deleting user, or a { uid, name } stamp
17
+ * (the name is denormalized into the trash item)
18
+ * @param objectType - Type identifier for filtering (e.g., "djRequest")
19
+ * @param databaseId - Optional named database
20
+ * @returns The created trash item
21
+ * @throws NotFoundError if document doesn't exist
22
+ */
6
23
  async function moveToTrash(collection, documentId, deletedBy, objectType, databaseId) {
7
- const db = getDb(databaseId);
8
- const originalRef = db.collection(collection).doc(documentId);
9
- const trashRef = db.collection(TRASH_COLLECTION).doc();
10
- return db.runTransaction(async (transaction) => {
11
- const originalDoc = await transaction.get(originalRef);
12
- if (!originalDoc.exists) {
13
- throw new NotFoundError(`Document ${collection}/${documentId} not found`);
14
- }
15
- const originalData = originalDoc.data();
16
- const trashData = {
17
- originalId: documentId,
18
- originalCollection: collection,
19
- objectType,
20
- deletedAt: Timestamp.now(),
21
- deletedBy: typeof deletedBy === "string" ? deletedBy : deletedBy.uid,
22
- ...typeof deletedBy === "object" && {
23
- deletedByName: deletedBy.name
24
- },
25
- data: originalData
26
- };
27
- transaction.set(trashRef, trashData);
28
- transaction.delete(originalRef);
29
- return {
30
- id: trashRef.id,
31
- ...trashData
32
- };
33
- });
24
+ const db = getDb(databaseId);
25
+ const originalRef = db.collection(collection).doc(documentId);
26
+ const trashRef = db.collection(TRASH_COLLECTION).doc();
27
+ return db.runTransaction(async (transaction) => {
28
+ const originalDoc = await transaction.get(originalRef);
29
+ if (!originalDoc.exists) throw new NotFoundError(`Document ${collection}/${documentId} not found`);
30
+ const originalData = originalDoc.data();
31
+ const trashData = {
32
+ originalId: documentId,
33
+ originalCollection: collection,
34
+ objectType,
35
+ deletedAt: Timestamp.now(),
36
+ deletedBy: typeof deletedBy === "string" ? deletedBy : deletedBy.uid,
37
+ ...typeof deletedBy === "object" && { deletedByName: deletedBy.name },
38
+ data: originalData
39
+ };
40
+ transaction.set(trashRef, trashData);
41
+ transaction.delete(originalRef);
42
+ return {
43
+ id: trashRef.id,
44
+ ...trashData
45
+ };
46
+ });
34
47
  }
48
+ /**
49
+ * Restore a document from trash to its original location.
50
+ *
51
+ * Atomically:
52
+ * 1. Reads the trash entry
53
+ * 2. Recreates the original document
54
+ * 3. Deletes the trash entry
55
+ *
56
+ * @param trashItemId - The trash document ID
57
+ * @param databaseId - Optional named database
58
+ * @throws NotFoundError if trash item doesn't exist
59
+ */
35
60
  async function restoreFromTrash(trashItemId, databaseId) {
36
- const db = getDb(databaseId);
37
- const trashRef = db.collection(TRASH_COLLECTION).doc(trashItemId);
38
- await db.runTransaction(async (transaction) => {
39
- const trashDoc = await transaction.get(trashRef);
40
- if (!trashDoc.exists) {
41
- throw new NotFoundError(`Trash item ${trashItemId} not found`);
42
- }
43
- const trashData = trashDoc.data();
44
- const originalRef = db.collection(trashData.originalCollection).doc(trashData.originalId);
45
- transaction.set(originalRef, trashData.data);
46
- transaction.delete(trashRef);
47
- });
61
+ const db = getDb(databaseId);
62
+ const trashRef = db.collection(TRASH_COLLECTION).doc(trashItemId);
63
+ await db.runTransaction(async (transaction) => {
64
+ const trashDoc = await transaction.get(trashRef);
65
+ if (!trashDoc.exists) throw new NotFoundError(`Trash item ${trashItemId} not found`);
66
+ const trashData = trashDoc.data();
67
+ const originalRef = db.collection(trashData.originalCollection).doc(trashData.originalId);
68
+ transaction.set(originalRef, trashData.data);
69
+ transaction.delete(trashRef);
70
+ });
48
71
  }
72
+ /**
73
+ * Permanently delete a document from trash.
74
+ *
75
+ * @param trashItemId - The trash document ID
76
+ * @param databaseId - Optional named database
77
+ * @throws NotFoundError if trash item doesn't exist
78
+ */
49
79
  async function permanentDelete(trashItemId, databaseId) {
50
- const db = getDb(databaseId);
51
- const trashRef = db.collection(TRASH_COLLECTION).doc(trashItemId);
52
- const trashDoc = await trashRef.get();
53
- if (!trashDoc.exists) {
54
- throw new NotFoundError(`Trash item ${trashItemId} not found`);
55
- }
56
- await trashRef.delete();
80
+ const trashRef = getDb(databaseId).collection(TRASH_COLLECTION).doc(trashItemId);
81
+ if (!(await trashRef.get()).exists) throw new NotFoundError(`Trash item ${trashItemId} not found`);
82
+ await trashRef.delete();
57
83
  }
84
+ /**
85
+ * List items in trash, optionally filtered by object type.
86
+ *
87
+ * @param objectType - Optional filter by object type
88
+ * @param limit - Maximum items to return (default: 100)
89
+ * @param databaseId - Optional named database
90
+ * @returns Array of trash items, newest first
91
+ */
58
92
  async function listTrash(objectType, limit = 100, databaseId) {
59
- const db = getDb(databaseId);
60
- let query = db.collection(TRASH_COLLECTION).orderBy("deletedAt", "desc").limit(limit);
61
- if (objectType) {
62
- query = query.where("objectType", "==", objectType);
63
- }
64
- const snapshot = await query.get();
65
- return snapshot.docs.map((doc) => ({
66
- id: doc.id,
67
- ...doc.data()
68
- }));
93
+ let query = getDb(databaseId).collection(TRASH_COLLECTION).orderBy("deletedAt", "desc").limit(limit);
94
+ if (objectType) query = query.where("objectType", "==", objectType);
95
+ return (await query.get()).docs.map((doc) => ({
96
+ id: doc.id,
97
+ ...doc.data()
98
+ }));
69
99
  }
100
+ /**
101
+ * Get a single trash item by ID.
102
+ *
103
+ * @param trashItemId - The trash document ID
104
+ * @param databaseId - Optional named database
105
+ * @returns The trash item or null if not found
106
+ */
70
107
  async function getTrashItem(trashItemId, databaseId) {
71
- const db = getDb(databaseId);
72
- const trashRef = db.collection(TRASH_COLLECTION).doc(trashItemId);
73
- const doc = await trashRef.get();
74
- if (!doc.exists) {
75
- return null;
76
- }
77
- return {
78
- id: doc.id,
79
- ...doc.data()
80
- };
108
+ const doc = await getDb(databaseId).collection(TRASH_COLLECTION).doc(trashItemId).get();
109
+ if (!doc.exists) return null;
110
+ return {
111
+ id: doc.id,
112
+ ...doc.data()
113
+ };
81
114
  }
82
-
115
+ //#endregion
83
116
  export { getTrashItem, listTrash, moveToTrash, permanentDelete, restoreFromTrash };
package/dist/types.js CHANGED
@@ -1,11 +1,12 @@
1
- const StatusCode = {
2
- OK: 200,
3
- CREATED: 201,
4
- BAD_REQUEST: 400,
5
- UNAUTHORIZED: 401,
6
- FORBIDDEN: 403,
7
- NOT_FOUND: 404,
8
- INTERNAL_ERROR: 500
1
+ //#region src/types.ts
2
+ var StatusCode = {
3
+ OK: 200,
4
+ CREATED: 201,
5
+ BAD_REQUEST: 400,
6
+ UNAUTHORIZED: 401,
7
+ FORBIDDEN: 403,
8
+ NOT_FOUND: 404,
9
+ INTERNAL_ERROR: 500
9
10
  };
10
-
11
+ //#endregion
11
12
  export { StatusCode };
package/dist/visits.js CHANGED
@@ -1,105 +1,147 @@
1
- import { createHash } from 'node:crypto';
2
- import { Timestamp, FieldValue } from 'firebase-admin/firestore';
3
- import { getDb } from './firestore.js';
4
-
5
- const DEVICES = ["phone", "tablet", "desktop"];
6
- const BOTS = /bot|crawl|spider|slurp|preview|monitor|headless|lighthouse|curl|wget/i;
7
- const ALREADY_EXISTS = 6;
1
+ import { getDb } from "./firestore.js";
2
+ import { FieldValue, Timestamp } from "firebase-admin/firestore";
3
+ import { createHash } from "node:crypto";
4
+ //#region src/visits.ts
5
+ var DEVICES = [
6
+ "phone",
7
+ "tablet",
8
+ "desktop"
9
+ ];
10
+ /** Deliberately short: this keeps out the loudest noise, it is not a bot list. */
11
+ var BOTS = /bot|crawl|spider|slurp|preview|monitor|headless|lighthouse|curl|wget/i;
12
+ /** Firestore ALREADY_EXISTS — a gRPC status code, not an HTTP one. */
13
+ var ALREADY_EXISTS = 6;
14
+ /**
15
+ * The day as YYYY-MM-DD in the given zone.
16
+ *
17
+ * "sv-SE" is the shortest way to an ISO-shaped date: `toISOString()` would be
18
+ * UTC, and building it by hand from parts is where off-by-one bugs live.
19
+ */
8
20
  function visitDay(now, timeZone) {
9
- return new Intl.DateTimeFormat("sv-SE", { timeZone }).format(now);
21
+ return new Intl.DateTimeFormat("sv-SE", { timeZone }).format(now);
10
22
  }
23
+ /** The stretch of time a visitor key stays the same. */
11
24
  function visitPeriod(day, rotate) {
12
- return rotate === "month" ? day.slice(0, 7) : day;
25
+ return rotate === "month" ? day.slice(0, 7) : day;
13
26
  }
27
+ /** The technical key — short enough to read, long enough not to collide. */
14
28
  function visitorKey(period, ip, userAgent, secret = "") {
15
- return createHash("sha256").update(`${secret}|${period}|${ip}|${userAgent}`).digest("hex").slice(0, 16);
29
+ return createHash("sha256").update(`${secret}|${period}|${ip}|${userAgent}`).digest("hex").slice(0, 16);
16
30
  }
31
+ /** Unknown values are dropped, never guessed and never stored raw. */
17
32
  function cleanDevice(value) {
18
- return typeof value === "string" && DEVICES.includes(value) ? value : void 0;
33
+ return typeof value === "string" && DEVICES.includes(value) ? value : void 0;
19
34
  }
35
+ /** A host, nothing else — no path, no query, no room for someone else's data. */
20
36
  function cleanReferrer(value) {
21
- if (typeof value !== "string") return void 0;
22
- const host = value.trim().toLowerCase();
23
- return /^[a-z0-9.\-:]{1,60}$/.test(host) ? host : void 0;
37
+ if (typeof value !== "string") return void 0;
38
+ const host = value.trim().toLowerCase();
39
+ return /^[a-z0-9.\-:]{1,60}$/.test(host) ? host : void 0;
24
40
  }
25
41
  function isBot(userAgent) {
26
- return BOTS.test(userAgent);
42
+ return BOTS.test(userAgent);
27
43
  }
44
+ /**
45
+ * Browser family and major version.
46
+ *
47
+ * ORDER IS THE WHOLE TRICK, because these strings quote each other: Edge says
48
+ * Edg AND Chrome AND Safari, Chrome says Chrome AND Safari. So the most
49
+ * specific claim wins, and Safari is asked last. On iOS every engine is
50
+ * Safari's, but the browsers name themselves (CriOS, FxiOS) — reported as what
51
+ * the user sees, since that is what the question is about.
52
+ *
53
+ * Unclassified traffic becomes "other" rather than nothing: an unreadable
54
+ * agent is a fact worth seeing, missing data is a different fact.
55
+ */
28
56
  function browserOf(userAgent) {
29
- const rules = [
30
- ["Edge", /Edg(?:iOS|A)?\/(\d+)/],
31
- ["Opera", /OPR\/(\d+)/],
32
- ["Samsung Internet", /SamsungBrowser\/(\d+)/],
33
- ["Chrome", /CriOS\/(\d+)/],
34
- ["Firefox", /FxiOS\/(\d+)/],
35
- ["Firefox", /Firefox\/(\d+)/],
36
- ["Chrome", /Chrome\/(\d+)/],
37
- ["Safari", /Version\/(\d+)[\d.]*\s+(?:Mobile\/\S+\s+)?Safari/]
38
- ];
39
- for (const [browser, pattern] of rules) {
40
- const hit = pattern.exec(userAgent);
41
- if (hit) return { browser, version: hit[1] };
42
- }
43
- return { browser: "other" };
57
+ for (const [browser, pattern] of [
58
+ ["Edge", /Edg(?:iOS|A)?\/(\d+)/],
59
+ ["Opera", /OPR\/(\d+)/],
60
+ ["Samsung Internet", /SamsungBrowser\/(\d+)/],
61
+ ["Chrome", /CriOS\/(\d+)/],
62
+ ["Firefox", /FxiOS\/(\d+)/],
63
+ ["Firefox", /Firefox\/(\d+)/],
64
+ ["Chrome", /Chrome\/(\d+)/],
65
+ ["Safari", /Version\/(\d+)[\d.]*\s+(?:Mobile\/\S+\s+)?Safari/]
66
+ ]) {
67
+ const hit = pattern.exec(userAgent);
68
+ if (hit) return {
69
+ browser,
70
+ version: hit[1]
71
+ };
72
+ }
73
+ return { browser: "other" };
44
74
  }
75
+ /**
76
+ * Operating system family, never a version.
77
+ *
78
+ * Android carries "Linux" in its agent and an iPad in desktop mode calls
79
+ * itself a Macintosh — so the order matters here too, and the iPad stays
80
+ * wrong. That is a limit of the source, not something to guess around.
81
+ */
45
82
  function osOf(userAgent) {
46
- if (/iPhone|iPad|iPod/.test(userAgent)) return "iOS";
47
- if (/Android/.test(userAgent)) return "Android";
48
- if (/CrOS/.test(userAgent)) return "ChromeOS";
49
- if (/Windows/.test(userAgent)) return "Windows";
50
- if (/Macintosh|Mac OS X/.test(userAgent)) return "macOS";
51
- if (/Linux/.test(userAgent)) return "Linux";
52
- return "other";
83
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "iOS";
84
+ if (/Android/.test(userAgent)) return "Android";
85
+ if (/CrOS/.test(userAgent)) return "ChromeOS";
86
+ if (/Windows/.test(userAgent)) return "Windows";
87
+ if (/Macintosh|Mac OS X/.test(userAgent)) return "macOS";
88
+ if (/Linux/.test(userAgent)) return "Linux";
89
+ return "other";
53
90
  }
91
+ /** No list means no restriction; an empty match means this call does not count. */
54
92
  function originAllowed(origin, allowed) {
55
- if (!allowed) return true;
56
- const list = Array.isArray(allowed) ? allowed : [allowed];
57
- return typeof origin === "string" && list.includes(origin);
93
+ if (!allowed) return true;
94
+ return typeof origin === "string" && (Array.isArray(allowed) ? allowed : [allowed]).includes(origin);
58
95
  }
96
+ /** First entry of x-forwarded-for; the rest are proxies. */
59
97
  function clientIp(headers) {
60
- const raw = headers["x-forwarded-for"];
61
- const value = Array.isArray(raw) ? raw[0] : raw;
62
- if (typeof value !== "string") return void 0;
63
- const first = value.split(",")[0]?.trim();
64
- return first || void 0;
98
+ const raw = headers["x-forwarded-for"];
99
+ const value = Array.isArray(raw) ? raw[0] : raw;
100
+ if (typeof value !== "string") return void 0;
101
+ return value.split(",")[0]?.trim() || void 0;
65
102
  }
103
+ /**
104
+ * Count one visit — or recognise that this visitor was already counted today.
105
+ *
106
+ * Never throws for the ordinary "does not count" cases (wrong origin, bot, no
107
+ * address, already seen): the caller is a fire-and-forget call from a page
108
+ * that must not care.
109
+ */
66
110
  async function countVisit(request, visit = {}, options = {}) {
67
- const headers = request.rawRequest?.headers ?? {};
68
- const day = visitDay(/* @__PURE__ */ new Date(), options.timeZone ?? "UTC");
69
- const skip = { counted: false, day };
70
- if (!originAllowed(headers.origin, options.origin)) return skip;
71
- const userAgent = typeof headers["user-agent"] === "string" ? headers["user-agent"] : "";
72
- if (!userAgent || isBot(userAgent)) return skip;
73
- const ip = clientIp(headers);
74
- if (!ip) return skip;
75
- const { browser, version } = browserOf(userAgent);
76
- const period = visitPeriod(day, options.rotate ?? "day");
77
- const visitor = visitorKey(period, ip, userAgent, options.secret);
78
- try {
79
- await getDb(options.databaseId).collection(options.collection ?? "visits").doc(`${day}_${visitor}`).create({
80
- day,
81
- visitor,
82
- at: FieldValue.serverTimestamp(),
83
- // Missing is missing: getDb ignores undefined, so an absent value
84
- // leaves no empty field behind.
85
- device: cleanDevice(visit.device),
86
- standalone: visit.standalone === true,
87
- referrer: cleanReferrer(visit.referrer),
88
- // Read from the agent the server already has — the client sends
89
- // nothing for these. Family and MAJOR version only; the full string
90
- // stays where it cannot be read back, in the key.
91
- browser,
92
- browserVersion: version,
93
- os: osOf(userAgent),
94
- expires: options.retentionDays ? Timestamp.fromMillis(
95
- Date.now() + options.retentionDays * 864e5
96
- ) : void 0
97
- });
98
- return { counted: true, day };
99
- } catch (e) {
100
- if (e?.code === ALREADY_EXISTS) return skip;
101
- throw e;
102
- }
111
+ const headers = request.rawRequest?.headers ?? {};
112
+ const day = visitDay(/* @__PURE__ */ new Date(), options.timeZone ?? "UTC");
113
+ const skip = {
114
+ counted: false,
115
+ day
116
+ };
117
+ if (!originAllowed(headers.origin, options.origin)) return skip;
118
+ const userAgent = typeof headers["user-agent"] === "string" ? headers["user-agent"] : "";
119
+ if (!userAgent || isBot(userAgent)) return skip;
120
+ const ip = clientIp(headers);
121
+ if (!ip) return skip;
122
+ const { browser, version } = browserOf(userAgent);
123
+ const visitor = visitorKey(visitPeriod(day, options.rotate ?? "day"), ip, userAgent, options.secret);
124
+ try {
125
+ await getDb(options.databaseId).collection(options.collection ?? "visits").doc(`${day}_${visitor}`).create({
126
+ day,
127
+ visitor,
128
+ at: FieldValue.serverTimestamp(),
129
+ device: cleanDevice(visit.device),
130
+ standalone: visit.standalone === true,
131
+ referrer: cleanReferrer(visit.referrer),
132
+ browser,
133
+ browserVersion: version,
134
+ os: osOf(userAgent),
135
+ expires: options.retentionDays ? Timestamp.fromMillis(Date.now() + options.retentionDays * 864e5) : void 0
136
+ });
137
+ return {
138
+ counted: true,
139
+ day
140
+ };
141
+ } catch (e) {
142
+ if (e?.code === ALREADY_EXISTS) return skip;
143
+ throw e;
144
+ }
103
145
  }
104
-
146
+ //#endregion
105
147
  export { browserOf, cleanDevice, cleanReferrer, clientIp, countVisit, isBot, originAllowed, osOf, visitDay, visitPeriod, visitorKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@progressive-development/pd-provider-firebase-functions",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Firebase Functions v2 utilities for pd-spa-helper backend",
5
5
  "author": "PD Progressive Development",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -17,13 +17,12 @@
17
17
  "README.md",
18
18
  "LICENSE"
19
19
  ],
20
- "dependencies": {
21
- "firebase-admin": "^12.6.0",
22
- "firebase-functions": "^6.0.0"
20
+ "engines": {
21
+ "node": "22"
23
22
  },
24
23
  "peerDependencies": {
25
- "firebase-admin": "^12.6.0",
26
- "firebase-functions": "^6.0.0"
24
+ "firebase-admin": ">=12.6.0 <15",
25
+ "firebase-functions": ">=6 <8"
27
26
  },
28
27
  "keywords": [
29
28
  "pd",
@@ -33,12 +32,16 @@
33
32
  "functions",
34
33
  "backend"
35
34
  ],
35
+ "devDependencies": {
36
+ "firebase-admin": "^14.3.0",
37
+ "firebase-functions": "^7.3.2"
38
+ },
36
39
  "scripts": {
37
40
  "build": "vite build",
38
41
  "clean": "rm -rf dist",
39
42
  "clean:all": "rm -rf dist node_modules pnpm-lock.yaml",
40
- "lint": "eslint --ext .ts src --ignore-path ../../.eslintignore && prettier \"**/*.ts\" --check --ignore-path ../../.eslintignore",
41
- "format": "eslint --ext .ts src --fix --ignore-path ../../.eslintignore && prettier \"**/*.ts\" --write --ignore-path ../../.eslintignore",
43
+ "lint": "eslint src && prettier \"**/*.ts\" --check --ignore-path ../../.eslintignore",
44
+ "format": "eslint src --fix && prettier \"**/*.ts\" --write --ignore-path ../../.eslintignore",
42
45
  "check": "pnpm run lint && pnpm run build",
43
46
  "test": "vitest run"
44
47
  }