@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/mail.js CHANGED
@@ -1,42 +1,63 @@
1
- import { Timestamp } from 'firebase-admin/firestore';
2
- import { getDb } from './firestore.js';
3
- import { ValidationError } from './errors.js';
4
-
5
- const DEFAULT_MAIL_COLLECTION = "mail";
1
+ import { ValidationError } from "./errors.js";
2
+ import { getDb } from "./firestore.js";
3
+ import { Timestamp } from "firebase-admin/firestore";
4
+ //#region src/mail.ts
5
+ var DEFAULT_MAIL_COLLECTION = "mail";
6
+ /**
7
+ * Write a mail document for the Firebase mail extension to send.
8
+ *
9
+ * Throws on failure — the calling app decides whether a mail error aborts
10
+ * its flow or is just logged.
11
+ *
12
+ * @param mail - The mail data; app-specific extra fields are stored as-is
13
+ * @param collection - Mail collection name (default: "mail")
14
+ * @param databaseId - Optional named database
15
+ * @returns The created mail document ID
16
+ * @throws ValidationError if `to` is missing or neither template nor message is set
17
+ */
6
18
  async function addMailForSend(mail, collection = DEFAULT_MAIL_COLLECTION, databaseId) {
7
- if (!mail.to) {
8
- throw new ValidationError("Mail requires a recipient (to)");
9
- }
10
- if (!mail.template && !mail.message) {
11
- throw new ValidationError("Mail requires a template or a message");
12
- }
13
- const db = getDb(databaseId);
14
- const docRef = await db.collection(collection).add({
15
- ...mail,
16
- creation: Timestamp.now()
17
- });
18
- return docRef.id;
19
+ if (!mail.to) throw new ValidationError("Mail requires a recipient (to)");
20
+ if (!mail.template && !mail.message) throw new ValidationError("Mail requires a template or a message");
21
+ return (await getDb(databaseId).collection(collection).add({
22
+ ...mail,
23
+ creation: Timestamp.now()
24
+ })).id;
19
25
  }
26
+ /**
27
+ * Get a single mail document by ID (e.g. to inspect delivery state).
28
+ *
29
+ * @param mailId - The mail document ID
30
+ * @param collection - Mail collection name (default: "mail")
31
+ * @param databaseId - Optional named database
32
+ * @returns The stored mail or null if not found
33
+ */
20
34
  async function getMailById(mailId, collection = DEFAULT_MAIL_COLLECTION, databaseId) {
21
- const db = getDb(databaseId);
22
- const doc = await db.collection(collection).doc(mailId).get();
23
- if (!doc.exists) {
24
- return null;
25
- }
26
- return {
27
- id: doc.id,
28
- ...doc.data()
29
- };
35
+ const doc = await getDb(databaseId).collection(collection).doc(mailId).get();
36
+ if (!doc.exists) return null;
37
+ return {
38
+ id: doc.id,
39
+ ...doc.data()
40
+ };
30
41
  }
42
+ /**
43
+ * List mail documents by an app-specific correlation field
44
+ * (e.g. field "eventId").
45
+ *
46
+ * Deliberately without orderBy — no composite index requirement per queried
47
+ * field; sorting is up to the caller.
48
+ *
49
+ * @param field - Field name to filter on
50
+ * @param value - Value the field must equal
51
+ * @param collection - Mail collection name (default: "mail")
52
+ * @param limit - Maximum documents to return (default: 100)
53
+ * @param databaseId - Optional named database
54
+ * @returns Array of stored mails
55
+ */
31
56
  async function getMailsByField(field, value, collection = DEFAULT_MAIL_COLLECTION, limit = 100, databaseId) {
32
- const db = getDb(databaseId);
33
- const snapshot = await db.collection(collection).where(field, "==", value).limit(limit).get();
34
- return snapshot.docs.map(
35
- (doc) => ({
36
- id: doc.id,
37
- ...doc.data()
38
- })
39
- );
57
+ return (await getDb(databaseId).collection(collection).where(field, "==", value).limit(limit).get()).docs.map((doc) => ({
58
+ id: doc.id,
59
+ ...doc.data()
60
+ }));
40
61
  }
41
-
62
+ //#endregion
42
63
  export { addMailForSend, getMailById, getMailsByField };
package/dist/response.js CHANGED
@@ -1,33 +1,42 @@
1
- import { StatusCode } from './types.js';
2
- import { AppError } from './errors.js';
3
- import { logger } from './logger.js';
4
-
1
+ import { StatusCode } from "./types.js";
2
+ import { AppError } from "./errors.js";
3
+ import { logger } from "./logger.js";
4
+ //#region src/response.ts
5
5
  function success(data) {
6
- return { statusCode: StatusCode.OK, resultData: data };
6
+ return {
7
+ statusCode: StatusCode.OK,
8
+ resultData: data
9
+ };
7
10
  }
8
11
  function created(data) {
9
- return { statusCode: StatusCode.CREATED, resultData: data };
12
+ return {
13
+ statusCode: StatusCode.CREATED,
14
+ resultData: data
15
+ };
10
16
  }
11
17
  function handleError(error) {
12
- if (error instanceof AppError) {
13
- if (error.statusCode >= 500) {
14
- logger.error("App error", error);
15
- } else {
16
- logger.warn("Request error", {
17
- code: error.code,
18
- message: error.message
19
- });
20
- }
21
- return {
22
- statusCode: error.statusCode,
23
- resultData: { error: error.message, code: error.code }
24
- };
25
- }
26
- logger.error("Unexpected error", error);
27
- return {
28
- statusCode: StatusCode.INTERNAL_ERROR,
29
- resultData: { error: "Internal server error", code: "INTERNAL_ERROR" }
30
- };
18
+ if (error instanceof AppError) {
19
+ if (error.statusCode >= 500) logger.error("App error", error);
20
+ else logger.warn("Request error", {
21
+ code: error.code,
22
+ message: error.message
23
+ });
24
+ return {
25
+ statusCode: error.statusCode,
26
+ resultData: {
27
+ error: error.message,
28
+ code: error.code
29
+ }
30
+ };
31
+ }
32
+ logger.error("Unexpected error", error);
33
+ return {
34
+ statusCode: StatusCode.INTERNAL_ERROR,
35
+ resultData: {
36
+ error: "Internal server error",
37
+ code: "INTERNAL_ERROR"
38
+ }
39
+ };
31
40
  }
32
-
41
+ //#endregion
33
42
  export { created, handleError, success };
@@ -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 };