@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/bin/prepare-functions-deploy.mjs +141 -0
- package/bin/prepare-functions-deploy.spec.mjs +108 -0
- package/dist/auth-trash.js +118 -107
- package/dist/auth.js +30 -23
- package/dist/changes.js +53 -60
- package/dist/counter.js +19 -17
- package/dist/errors.js +41 -40
- package/dist/firestore.js +17 -11
- package/dist/history.js +74 -45
- package/dist/import-templates.js +102 -106
- package/dist/index.js +16 -15
- package/dist/logger.js +11 -8
- package/dist/mail.js +56 -35
- package/dist/response.js +35 -26
- package/dist/script-helpers.js +55 -44
- package/dist/trash.js +105 -72
- package/dist/types.js +10 -9
- package/dist/visits.js +124 -82
- package/package.json +15 -8
package/dist/mail.js
CHANGED
|
@@ -1,42 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getDb } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
2
|
-
import { AppError } from
|
|
3
|
-
import { logger } from
|
|
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
|
-
|
|
6
|
+
return {
|
|
7
|
+
statusCode: StatusCode.OK,
|
|
8
|
+
resultData: data
|
|
9
|
+
};
|
|
7
10
|
}
|
|
8
11
|
function created(data) {
|
|
9
|
-
|
|
12
|
+
return {
|
|
13
|
+
statusCode: StatusCode.CREATED,
|
|
14
|
+
resultData: data
|
|
15
|
+
};
|
|
10
16
|
}
|
|
11
17
|
function handleError(error) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 };
|
package/dist/script-helpers.js
CHANGED
|
@@ -1,52 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 {
|
|
2
|
-
import { getDb } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 };
|