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

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/counter.js CHANGED
@@ -1,20 +1,22 @@
1
- import { getDb } from './firestore.js';
2
-
3
- const COUNTERS_COLLECTION = "counters";
1
+ import { getDb } from "./firestore.js";
2
+ //#region src/counter.ts
3
+ var COUNTERS_COLLECTION = "counters";
4
+ /**
5
+ * Get next counter value atomically
6
+ * Uses Firestore transaction to prevent race conditions
7
+ */
4
8
  async function getNextCounter(counterName, databaseId) {
5
- const db = getDb(databaseId);
6
- const counterRef = db.collection(COUNTERS_COLLECTION).doc(counterName);
7
- return db.runTransaction(async (transaction) => {
8
- const doc = await transaction.get(counterRef);
9
- const current = doc.exists ? doc.data()?.value ?? 0 : 0;
10
- const next = current + 1;
11
- transaction.set(
12
- counterRef,
13
- { value: next, updatedAt: /* @__PURE__ */ new Date() },
14
- { merge: true }
15
- );
16
- return next;
17
- });
9
+ const db = getDb(databaseId);
10
+ const counterRef = db.collection(COUNTERS_COLLECTION).doc(counterName);
11
+ return db.runTransaction(async (transaction) => {
12
+ const doc = await transaction.get(counterRef);
13
+ const next = (doc.exists ? doc.data()?.value ?? 0 : 0) + 1;
14
+ transaction.set(counterRef, {
15
+ value: next,
16
+ updatedAt: /* @__PURE__ */ new Date()
17
+ }, { merge: true });
18
+ return next;
19
+ });
18
20
  }
19
-
21
+ //#endregion
20
22
  export { getNextCounter };
package/dist/errors.js CHANGED
@@ -1,41 +1,42 @@
1
- const SC_EMAIL_ALREADY_USED = -160;
2
- class AppError extends Error {
3
- constructor(message, code, statusCode) {
4
- super(message);
5
- this.code = code;
6
- this.statusCode = statusCode;
7
- this.name = this.constructor.name;
8
- }
9
- }
10
- class ValidationError extends AppError {
11
- constructor(message) {
12
- super(message, "VALIDATION_ERROR", 400);
13
- }
14
- }
15
- class UnauthorizedError extends AppError {
16
- constructor(message = "Authentication required") {
17
- super(message, "UNAUTHORIZED", 401);
18
- }
19
- }
20
- class ForbiddenError extends AppError {
21
- constructor(message = "Access denied") {
22
- super(message, "FORBIDDEN", 403);
23
- }
24
- }
25
- class NotFoundError extends AppError {
26
- constructor(message) {
27
- super(message, "NOT_FOUND", 404);
28
- }
29
- }
30
- class ConflictError extends AppError {
31
- constructor(message, code = "CONFLICT") {
32
- super(message, code, 409);
33
- }
34
- }
35
- class EmailAlreadyUsedError extends AppError {
36
- constructor() {
37
- super("Email already used", "EMAIL_ALREADY_USED", SC_EMAIL_ALREADY_USED);
38
- }
39
- }
40
-
1
+ //#region src/errors.ts
2
+ var SC_EMAIL_ALREADY_USED = -160;
3
+ var AppError = class extends Error {
4
+ constructor(message, code, statusCode) {
5
+ super(message);
6
+ this.code = code;
7
+ this.statusCode = statusCode;
8
+ this.name = this.constructor.name;
9
+ }
10
+ };
11
+ var ValidationError = class extends AppError {
12
+ constructor(message) {
13
+ super(message, "VALIDATION_ERROR", 400);
14
+ }
15
+ };
16
+ var UnauthorizedError = class extends AppError {
17
+ constructor(message = "Authentication required") {
18
+ super(message, "UNAUTHORIZED", 401);
19
+ }
20
+ };
21
+ var ForbiddenError = class extends AppError {
22
+ constructor(message = "Access denied") {
23
+ super(message, "FORBIDDEN", 403);
24
+ }
25
+ };
26
+ var NotFoundError = class extends AppError {
27
+ constructor(message) {
28
+ super(message, "NOT_FOUND", 404);
29
+ }
30
+ };
31
+ var ConflictError = class extends AppError {
32
+ constructor(message, code = "CONFLICT") {
33
+ super(message, code, 409);
34
+ }
35
+ };
36
+ var EmailAlreadyUsedError = class extends AppError {
37
+ constructor() {
38
+ super("Email already used", "EMAIL_ALREADY_USED", SC_EMAIL_ALREADY_USED);
39
+ }
40
+ };
41
+ //#endregion
41
42
  export { AppError, ConflictError, EmailAlreadyUsedError, ForbiddenError, NotFoundError, SC_EMAIL_ALREADY_USED, UnauthorizedError, ValidationError };
package/dist/firestore.js CHANGED
@@ -1,14 +1,20 @@
1
- import { getFirestore } from 'firebase-admin/firestore';
2
-
3
- const dbCache = /* @__PURE__ */ new Map();
1
+ import { getFirestore } from "firebase-admin/firestore";
2
+ //#region src/firestore.ts
3
+ var dbCache = /* @__PURE__ */ new Map();
4
+ /**
5
+ * Get Firestore instance (cached)
6
+ *
7
+ * Uses the default Firebase app - consumer must call initializeApp() first.
8
+ * @param databaseId Optional named database
9
+ */
4
10
  function getDb(databaseId) {
5
- const cacheKey = databaseId ?? "default";
6
- if (!dbCache.has(cacheKey)) {
7
- const db = databaseId ? getFirestore(databaseId) : getFirestore();
8
- db.settings({ ignoreUndefinedProperties: true });
9
- dbCache.set(cacheKey, db);
10
- }
11
- return dbCache.get(cacheKey);
11
+ const cacheKey = databaseId ?? "default";
12
+ if (!dbCache.has(cacheKey)) {
13
+ const db = databaseId ? getFirestore(databaseId) : getFirestore();
14
+ db.settings({ ignoreUndefinedProperties: true });
15
+ dbCache.set(cacheKey, db);
16
+ }
17
+ return dbCache.get(cacheKey);
12
18
  }
13
-
19
+ //#endregion
14
20
  export { getDb };
package/dist/history.js CHANGED
@@ -1,53 +1,82 @@
1
- import { Timestamp } from 'firebase-admin/firestore';
2
- import { getDb } from './firestore.js';
3
- import { ValidationError } from './errors.js';
4
- import { logger } from './logger.js';
5
-
6
- const HISTORY_COLLECTION = "_history";
7
- const HistoryKind = {
8
- CREATED: "created",
9
- UPDATED: "updated",
10
- DELETED: "deleted",
11
- RESTORED: "restored",
12
- PROCESS: "process"
1
+ import { ValidationError } from "./errors.js";
2
+ import { logger } from "./logger.js";
3
+ import { getDb } from "./firestore.js";
4
+ import { Timestamp } from "firebase-admin/firestore";
5
+ //#region src/history.ts
6
+ var HISTORY_COLLECTION = "_history";
7
+ /**
8
+ * Core event categories — fixed in the helper. App-specific semantics go
9
+ * into the free-form `action` field, NOT in here.
10
+ */
11
+ var HistoryKind = {
12
+ CREATED: "created",
13
+ UPDATED: "updated",
14
+ DELETED: "deleted",
15
+ RESTORED: "restored",
16
+ PROCESS: "process"
13
17
  };
18
+ /**
19
+ * Write a history entry.
20
+ *
21
+ * Throws on failure — use recordHistory() at app write sites where a
22
+ * history error must never fail the main operation.
23
+ *
24
+ * @param entry - The event data (at is set server-side)
25
+ * @param databaseId - Optional named database
26
+ * @returns The created history entry
27
+ * @throws ValidationError if the object reference is incomplete
28
+ */
14
29
  async function addHistoryEntry(entry, databaseId) {
15
- if (!entry.objectCollection || !entry.objectId || !entry.objectType) {
16
- throw new ValidationError(
17
- "History entry requires objectCollection, objectId and objectType"
18
- );
19
- }
20
- const db = getDb(databaseId);
21
- const entryData = {
22
- ...entry,
23
- at: Timestamp.now()
24
- };
25
- const docRef = await db.collection(HISTORY_COLLECTION).add(entryData);
26
- return {
27
- id: docRef.id,
28
- ...entryData
29
- };
30
+ if (!entry.objectCollection || !entry.objectId || !entry.objectType) throw new ValidationError("History entry requires objectCollection, objectId and objectType");
31
+ const db = getDb(databaseId);
32
+ const entryData = {
33
+ ...entry,
34
+ at: Timestamp.now()
35
+ };
36
+ return {
37
+ id: (await db.collection(HISTORY_COLLECTION).add(entryData)).id,
38
+ ...entryData
39
+ };
30
40
  }
41
+ /**
42
+ * Best-effort variant of addHistoryEntry for app write sites: catches ANY
43
+ * error, logs it as a warning and returns null. The app's main operation
44
+ * can never fail because of history recording.
45
+ *
46
+ * @param entry - The event data
47
+ * @param databaseId - Optional named database
48
+ * @returns The created history entry, or null if recording failed
49
+ */
31
50
  async function recordHistory(entry, databaseId) {
32
- try {
33
- return await addHistoryEntry(entry, databaseId);
34
- } catch (error) {
35
- logger.warn("recordHistory failed", {
36
- error: error instanceof Error ? error.message : String(error),
37
- objectCollection: entry.objectCollection,
38
- objectId: entry.objectId,
39
- kind: entry.kind
40
- });
41
- return null;
42
- }
51
+ try {
52
+ return await addHistoryEntry(entry, databaseId);
53
+ } catch (error) {
54
+ logger.warn("recordHistory failed", {
55
+ error: error instanceof Error ? error.message : String(error),
56
+ objectCollection: entry.objectCollection,
57
+ objectId: entry.objectId,
58
+ kind: entry.kind
59
+ });
60
+ return null;
61
+ }
43
62
  }
63
+ /**
64
+ * List the history of an object, newest first.
65
+ *
66
+ * Requires a composite index in the consuming project:
67
+ * objectCollection ASC, objectId ASC, at DESC on collection "_history".
68
+ *
69
+ * @param objectCollection - Collection of the object (e.g., "locations")
70
+ * @param objectId - Document ID of the object
71
+ * @param limit - Maximum entries to return (default: 100)
72
+ * @param databaseId - Optional named database
73
+ * @returns Array of history entries, newest first
74
+ */
44
75
  async function listHistory(objectCollection, objectId, limit = 100, databaseId) {
45
- const db = getDb(databaseId);
46
- const snapshot = await db.collection(HISTORY_COLLECTION).where("objectCollection", "==", objectCollection).where("objectId", "==", objectId).orderBy("at", "desc").limit(limit).get();
47
- return snapshot.docs.map((doc) => ({
48
- id: doc.id,
49
- ...doc.data()
50
- }));
76
+ return (await getDb(databaseId).collection(HISTORY_COLLECTION).where("objectCollection", "==", objectCollection).where("objectId", "==", objectId).orderBy("at", "desc").limit(limit).get()).docs.map((doc) => ({
77
+ id: doc.id,
78
+ ...doc.data()
79
+ }));
51
80
  }
52
-
81
+ //#endregion
53
82
  export { HistoryKind, addHistoryEntry, listHistory, recordHistory };
@@ -1,113 +1,109 @@
1
- import { existsSync, readdirSync, readFileSync } from 'fs';
2
- import { scriptLog, createScriptContext } from './script-helpers.js';
3
-
1
+ import { createScriptContext, scriptLog } from "./script-helpers.js";
2
+ import { existsSync, readFileSync, readdirSync } from "fs";
3
+ //#region src/import-templates.ts
4
+ /**
5
+ * Reusable Mail Templates Import
6
+ *
7
+ * Imports mail templates from a templates directory into Firestore.
8
+ * Each template consists of three files:
9
+ * - {name}.json — metadata (name, subject)
10
+ * - {name}.html.hbs — HTML template
11
+ * - {name}.txt.hbs — Text template
12
+ */
4
13
  function findTemplates(templatesDir) {
5
- const files = readdirSync(templatesDir);
6
- const jsonFiles = files.filter(
7
- (f) => f.endsWith(".json") && f !== "serviceAccountKey.json"
8
- );
9
- return jsonFiles.map((f) => f.replace(".json", ""));
14
+ return readdirSync(templatesDir).filter((f) => f.endsWith(".json") && f !== "serviceAccountKey.json").map((f) => f.replace(".json", ""));
10
15
  }
11
16
  function loadTemplate(templatesDir, name) {
12
- const jsonPath = `${templatesDir}/${name}.json`;
13
- const htmlPath = `${templatesDir}/${name}.html.hbs`;
14
- const textPath = `${templatesDir}/${name}.txt.hbs`;
15
- if (!existsSync(jsonPath)) {
16
- scriptLog(`Metadata not found: ${jsonPath}`, "error");
17
- return null;
18
- }
19
- if (!existsSync(htmlPath)) {
20
- scriptLog(`HTML template not found: ${htmlPath}`, "error");
21
- return null;
22
- }
23
- if (!existsSync(textPath)) {
24
- scriptLog(`Text template not found: ${textPath}`, "error");
25
- return null;
26
- }
27
- const metadata = JSON.parse(
28
- readFileSync(jsonPath, "utf-8")
29
- );
30
- const html = readFileSync(htmlPath, "utf-8");
31
- const text = readFileSync(textPath, "utf-8");
32
- return { ...metadata, html, text };
17
+ const jsonPath = `${templatesDir}/${name}.json`;
18
+ const htmlPath = `${templatesDir}/${name}.html.hbs`;
19
+ const textPath = `${templatesDir}/${name}.txt.hbs`;
20
+ if (!existsSync(jsonPath)) {
21
+ scriptLog(`Metadata not found: ${jsonPath}`, "error");
22
+ return null;
23
+ }
24
+ if (!existsSync(htmlPath)) {
25
+ scriptLog(`HTML template not found: ${htmlPath}`, "error");
26
+ return null;
27
+ }
28
+ if (!existsSync(textPath)) {
29
+ scriptLog(`Text template not found: ${textPath}`, "error");
30
+ return null;
31
+ }
32
+ const metadata = JSON.parse(readFileSync(jsonPath, "utf-8"));
33
+ const html = readFileSync(htmlPath, "utf-8");
34
+ const text = readFileSync(textPath, "utf-8");
35
+ return {
36
+ ...metadata,
37
+ html,
38
+ text
39
+ };
33
40
  }
41
+ /**
42
+ * Import mail templates into Firestore.
43
+ */
34
44
  async function runImportTemplates(config) {
35
- const {
36
- templatesDir,
37
- serviceAccountPath,
38
- databaseId,
39
- collection = "emailTemplates",
40
- templateFilter,
41
- dryRun = false
42
- } = config;
43
- if (!existsSync(templatesDir)) {
44
- scriptLog(`Templates directory not found: ${templatesDir}`, "error");
45
- process.exit(1);
46
- }
47
- const { db } = createScriptContext({
48
- title: "Mail Templates Import",
49
- serviceAccountPath,
50
- databaseId,
51
- dryRun
52
- });
53
- scriptLog(`Templates dir: ${templatesDir}`);
54
- scriptLog(`Target collection: ${collection}`);
55
- let templates = findTemplates(templatesDir);
56
- if (templateFilter) {
57
- templates = templates.filter((t) => t === templateFilter);
58
- if (templates.length === 0) {
59
- scriptLog(`Template not found: ${templateFilter}`, "error");
60
- process.exit(1);
61
- }
62
- }
63
- scriptLog(`Found ${templates.length} template(s): ${templates.join(", ")}`);
64
- console.log("");
65
- let successCount = 0;
66
- let errorCount = 0;
67
- for (const templateName of templates) {
68
- const template = loadTemplate(templatesDir, templateName);
69
- if (!template) {
70
- errorCount++;
71
- continue;
72
- }
73
- const docId = `${templateName}Id`;
74
- console.log(`
75
- ${templateName}`);
76
- console.log(` Document ID: ${docId}`);
77
- console.log(` Subject: ${template.subject}`);
78
- console.log(` HTML: ${template.html.length} chars`);
79
- console.log(` Text: ${template.text.length} chars`);
80
- if (!dryRun) {
81
- try {
82
- await db.collection(collection).doc(docId).set({
83
- name: template.name,
84
- subject: template.subject,
85
- html: template.html,
86
- text: template.text,
87
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
88
- });
89
- scriptLog(`Imported: ${templateName} -> ${docId}`, "success");
90
- successCount++;
91
- } catch (error) {
92
- scriptLog(`Failed to import ${templateName}: ${error}`, "error");
93
- errorCount++;
94
- }
95
- } else {
96
- scriptLog(`Would import: ${templateName} -> ${docId}`, "info");
97
- successCount++;
98
- }
99
- }
100
- console.log("\n" + "-".repeat(50));
101
- console.log(`
102
- Summary:`);
103
- console.log(` Success: ${successCount}`);
104
- console.log(` Errors: ${errorCount}`);
105
- if (dryRun) {
106
- console.log(`
107
- Run without --dry-run to apply changes.
108
- `);
109
- }
110
- process.exit(errorCount > 0 ? 1 : 0);
45
+ const { templatesDir, serviceAccountPath, databaseId, collection = "emailTemplates", templateFilter, dryRun = false } = config;
46
+ if (!existsSync(templatesDir)) {
47
+ scriptLog(`Templates directory not found: ${templatesDir}`, "error");
48
+ process.exit(1);
49
+ }
50
+ const { db } = createScriptContext({
51
+ title: "Mail Templates Import",
52
+ serviceAccountPath,
53
+ databaseId,
54
+ dryRun
55
+ });
56
+ scriptLog(`Templates dir: ${templatesDir}`);
57
+ scriptLog(`Target collection: ${collection}`);
58
+ let templates = findTemplates(templatesDir);
59
+ if (templateFilter) {
60
+ templates = templates.filter((t) => t === templateFilter);
61
+ if (templates.length === 0) {
62
+ scriptLog(`Template not found: ${templateFilter}`, "error");
63
+ process.exit(1);
64
+ }
65
+ }
66
+ scriptLog(`Found ${templates.length} template(s): ${templates.join(", ")}`);
67
+ console.log("");
68
+ let successCount = 0;
69
+ let errorCount = 0;
70
+ for (const templateName of templates) {
71
+ const template = loadTemplate(templatesDir, templateName);
72
+ if (!template) {
73
+ errorCount++;
74
+ continue;
75
+ }
76
+ const docId = `${templateName}Id`;
77
+ console.log(`\n ${templateName}`);
78
+ console.log(` Document ID: ${docId}`);
79
+ console.log(` Subject: ${template.subject}`);
80
+ console.log(` HTML: ${template.html.length} chars`);
81
+ console.log(` Text: ${template.text.length} chars`);
82
+ if (!dryRun) try {
83
+ await db.collection(collection).doc(docId).set({
84
+ name: template.name,
85
+ subject: template.subject,
86
+ html: template.html,
87
+ text: template.text,
88
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
89
+ });
90
+ scriptLog(`Imported: ${templateName} -> ${docId}`, "success");
91
+ successCount++;
92
+ } catch (error) {
93
+ scriptLog(`Failed to import ${templateName}: ${error}`, "error");
94
+ errorCount++;
95
+ }
96
+ else {
97
+ scriptLog(`Would import: ${templateName} -> ${docId}`, "info");
98
+ successCount++;
99
+ }
100
+ }
101
+ console.log("\n" + "-".repeat(50));
102
+ console.log(`\nSummary:`);
103
+ console.log(` Success: ${successCount}`);
104
+ console.log(` Errors: ${errorCount}`);
105
+ if (dryRun) console.log(`\n Run without --dry-run to apply changes.\n`);
106
+ process.exit(errorCount > 0 ? 1 : 0);
111
107
  }
112
-
108
+ //#endregion
113
109
  export { runImportTemplates };
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
- export { StatusCode } from './types.js';
2
- export { AppError, ConflictError, EmailAlreadyUsedError, ForbiddenError, NotFoundError, SC_EMAIL_ALREADY_USED, UnauthorizedError, ValidationError } from './errors.js';
3
- export { requireAuth, requireClaims, requireOwnership } from './auth.js';
4
- export { created, handleError, success } from './response.js';
5
- export { getDb } from './firestore.js';
6
- export { getNextCounter } from './counter.js';
7
- export { countVisit } from './visits.js';
8
- export { getTrashItem, listTrash, moveToTrash, permanentDelete, restoreFromTrash } from './trash.js';
9
- export { moveToTrashWithAuth, permanentDeleteWithAuth, restoreFromTrashWithAuth } from './auth-trash.js';
10
- export { computeChanges } from './changes.js';
11
- export { HistoryKind, addHistoryEntry, listHistory, recordHistory } from './history.js';
12
- export { addMailForSend, getMailById, getMailsByField } from './mail.js';
13
- export { logger } from './logger.js';
14
- export { createScriptContext, initFirebaseAdmin, scriptLog } from './script-helpers.js';
15
- export { runImportTemplates } from './import-templates.js';
1
+ import { StatusCode } from "./types.js";
2
+ import { AppError, ConflictError, EmailAlreadyUsedError, ForbiddenError, NotFoundError, SC_EMAIL_ALREADY_USED, UnauthorizedError, ValidationError } from "./errors.js";
3
+ import { logger } from "./logger.js";
4
+ import { requireAuth, requireClaims, requireOwnership } from "./auth.js";
5
+ import { created, handleError, success } from "./response.js";
6
+ import { getDb } from "./firestore.js";
7
+ import { getNextCounter } from "./counter.js";
8
+ import { countVisit } from "./visits.js";
9
+ import { getTrashItem, listTrash, moveToTrash, permanentDelete, restoreFromTrash } from "./trash.js";
10
+ import { moveToTrashWithAuth, permanentDeleteWithAuth, restoreFromTrashWithAuth } from "./auth-trash.js";
11
+ import { computeChanges } from "./changes.js";
12
+ import { HistoryKind, addHistoryEntry, listHistory, recordHistory } from "./history.js";
13
+ import { addMailForSend, getMailById, getMailsByField } from "./mail.js";
14
+ import { createScriptContext, initFirebaseAdmin, scriptLog } from "./script-helpers.js";
15
+ import { runImportTemplates } from "./import-templates.js";
16
+ export { AppError, ConflictError, EmailAlreadyUsedError, ForbiddenError, HistoryKind, NotFoundError, SC_EMAIL_ALREADY_USED, StatusCode, UnauthorizedError, ValidationError, addHistoryEntry, addMailForSend, computeChanges, countVisit, createScriptContext, created, getDb, getMailById, getMailsByField, getNextCounter, getTrashItem, handleError, initFirebaseAdmin, listHistory, listTrash, logger, moveToTrash, moveToTrashWithAuth, permanentDelete, permanentDeleteWithAuth, recordHistory, requireAuth, requireClaims, requireOwnership, restoreFromTrash, restoreFromTrashWithAuth, runImportTemplates, scriptLog, success };
package/dist/logger.js CHANGED
@@ -1,10 +1,13 @@
1
- import * as functions from 'firebase-functions/logger';
2
-
3
- const logger = {
4
- debug: (msg, data) => functions.debug(msg, data),
5
- info: (msg, data) => functions.info(msg, data),
6
- warn: (msg, data) => functions.warn(msg, data),
7
- error: (msg, error, data) => functions.error(msg, { error, ...data })
1
+ import * as functions from "firebase-functions/logger";
2
+ //#region src/logger.ts
3
+ var logger = {
4
+ debug: (msg, data) => functions.debug(msg, data),
5
+ info: (msg, data) => functions.info(msg, data),
6
+ warn: (msg, data) => functions.warn(msg, data),
7
+ error: (msg, error, data) => functions.error(msg, {
8
+ error,
9
+ ...data
10
+ })
8
11
  };
9
-
12
+ //#endregion
10
13
  export { logger };