@progressive-development/pd-provider-firebase-functions 1.0.1 → 1.1.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/changes.d.ts +31 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +69 -0
- package/dist/changes.spec.d.ts +2 -0
- package/dist/changes.spec.d.ts.map +1 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +53 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/mail.d.ts +79 -0
- package/dist/mail.d.ts.map +1 -0
- package/dist/mail.js +42 -0
- package/package.json +3 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single field change.
|
|
3
|
+
* Absent sides are stored as null (not undefined — ignoreUndefinedProperties
|
|
4
|
+
* would drop the key on a Firestore write).
|
|
5
|
+
*/
|
|
6
|
+
export interface FieldChange {
|
|
7
|
+
old: unknown;
|
|
8
|
+
new: unknown;
|
|
9
|
+
}
|
|
10
|
+
/** Field name → change. Granularity is the top-level field. */
|
|
11
|
+
export type FieldChanges = Record<string, FieldChange>;
|
|
12
|
+
export interface ComputeChangesOptions {
|
|
13
|
+
/** Top-level fields to skip when comparing (e.g. "lastEdit"). */
|
|
14
|
+
ignoreFields?: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compute which top-level fields changed between two object states.
|
|
18
|
+
*
|
|
19
|
+
* Values are compared deeply (nested objects, arrays), but changes are
|
|
20
|
+
* reported per top-level field with the full old/new value. Firestore value
|
|
21
|
+
* types are compared via their own isEqual(). undefined, null and missing
|
|
22
|
+
* keys are treated as the same "not set" state.
|
|
23
|
+
*
|
|
24
|
+
* @param before - Previous state; undefined means "object did not exist yet"
|
|
25
|
+
* @param after - New state
|
|
26
|
+
* @param options - Optional ignore list for noisy fields (e.g. "lastEdit")
|
|
27
|
+
* @returns The changed fields — or undefined if nothing changed, so
|
|
28
|
+
* "did anything change?" is a truthiness check on the result
|
|
29
|
+
*/
|
|
30
|
+
export declare function computeChanges(before: Record<string, unknown> | undefined, after: Record<string, unknown>, options?: ComputeChangesOptions): FieldChanges | undefined;
|
|
31
|
+
//# sourceMappingURL=changes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../src/changes.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;CACd;AAED,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAkFD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,YAAY,GAAG,SAAS,CAiB1B"}
|
package/dist/changes.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function hasIsEqual(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && typeof value.isEqual === "function";
|
|
3
|
+
}
|
|
4
|
+
function isUnset(value) {
|
|
5
|
+
return value === void 0 || value === null;
|
|
6
|
+
}
|
|
7
|
+
function isEqualValue(a, b) {
|
|
8
|
+
if (isUnset(a) && isUnset(b)) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (isUnset(a) || isUnset(b)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (a === b) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (typeof a === "number" && typeof b === "number" && Number.isNaN(a) && Number.isNaN(b)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (hasIsEqual(a) && hasIsEqual(b)) {
|
|
21
|
+
try {
|
|
22
|
+
return a.isEqual(b);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (hasIsEqual(a) || hasIsEqual(b)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (a instanceof Date && b instanceof Date) {
|
|
31
|
+
return a.getTime() === b.getTime();
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
34
|
+
return a.length === b.length && a.every((item, i) => isEqualValue(item, b[i]));
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
40
|
+
const aObj = a;
|
|
41
|
+
const bObj = b;
|
|
42
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(aObj), ...Object.keys(bObj)]);
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
if (!isEqualValue(aObj[key], bObj[key])) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function computeChanges(before, after, options) {
|
|
53
|
+
const ignore = new Set(options?.ignoreFields ?? []);
|
|
54
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before ?? {}), ...Object.keys(after)]);
|
|
55
|
+
const changes = {};
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
if (ignore.has(key)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const oldValue = before?.[key];
|
|
61
|
+
const newValue = after[key];
|
|
62
|
+
if (!isEqualValue(oldValue, newValue)) {
|
|
63
|
+
changes[key] = { old: oldValue ?? null, new: newValue ?? null };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return Object.keys(changes).length > 0 ? changes : void 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { computeChanges };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changes.spec.d.ts","sourceRoot":"","sources":["../src/changes.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Timestamp } from 'firebase-admin/firestore';
|
|
2
|
+
import { FieldChanges } from './changes';
|
|
3
|
+
/**
|
|
4
|
+
* Core event categories — fixed in the helper. App-specific semantics go
|
|
5
|
+
* into the free-form `action` field, NOT in here.
|
|
6
|
+
*/
|
|
7
|
+
export declare const HistoryKind: {
|
|
8
|
+
readonly CREATED: "created";
|
|
9
|
+
readonly UPDATED: "updated";
|
|
10
|
+
readonly DELETED: "deleted";
|
|
11
|
+
readonly RESTORED: "restored";
|
|
12
|
+
readonly PROCESS: "process";
|
|
13
|
+
};
|
|
14
|
+
export type HistoryKind = (typeof HistoryKind)[keyof typeof HistoryKind];
|
|
15
|
+
/**
|
|
16
|
+
* A history event for an object.
|
|
17
|
+
* History is an append-only event log per object, not a snapshot/version
|
|
18
|
+
* system. Entries survive deletion of the object.
|
|
19
|
+
*/
|
|
20
|
+
export interface HistoryEntry {
|
|
21
|
+
/** Auto-generated history document ID */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Collection of the object (e.g., "locations") */
|
|
24
|
+
objectCollection: string;
|
|
25
|
+
/** Document ID of the object */
|
|
26
|
+
objectId: string;
|
|
27
|
+
/** Object type for filtering, same convention as trash (e.g., "location") */
|
|
28
|
+
objectType: string;
|
|
29
|
+
/** Core event category */
|
|
30
|
+
kind: HistoryKind;
|
|
31
|
+
/** When the event was recorded (server-side) */
|
|
32
|
+
at: Timestamp;
|
|
33
|
+
/** App-specific action (vocabulary owned by the app, e.g., "verify") */
|
|
34
|
+
action?: string;
|
|
35
|
+
/** UID of the authenticated actor */
|
|
36
|
+
userId?: string;
|
|
37
|
+
/** Label for non-authenticated/system actors (e.g., "cron", "import-script") */
|
|
38
|
+
actorLabel?: string;
|
|
39
|
+
/** Channel/entry point (app vocabulary: "member-app", "admin", "cron", …) */
|
|
40
|
+
app?: string;
|
|
41
|
+
/** Free text for display */
|
|
42
|
+
message?: string;
|
|
43
|
+
/** Field diff, usually only for kind=updated */
|
|
44
|
+
changes?: FieldChanges;
|
|
45
|
+
}
|
|
46
|
+
/** Input for a new entry — everything except id and at. */
|
|
47
|
+
export type NewHistoryEntry = Omit<HistoryEntry, "id" | "at">;
|
|
48
|
+
/**
|
|
49
|
+
* Write a history entry.
|
|
50
|
+
*
|
|
51
|
+
* Throws on failure — use recordHistory() at app write sites where a
|
|
52
|
+
* history error must never fail the main operation.
|
|
53
|
+
*
|
|
54
|
+
* @param entry - The event data (at is set server-side)
|
|
55
|
+
* @param databaseId - Optional named database
|
|
56
|
+
* @returns The created history entry
|
|
57
|
+
* @throws ValidationError if the object reference is incomplete
|
|
58
|
+
*/
|
|
59
|
+
export declare function addHistoryEntry(entry: NewHistoryEntry, databaseId?: string): Promise<HistoryEntry>;
|
|
60
|
+
/**
|
|
61
|
+
* Best-effort variant of addHistoryEntry for app write sites: catches ANY
|
|
62
|
+
* error, logs it as a warning and returns null. The app's main operation
|
|
63
|
+
* can never fail because of history recording.
|
|
64
|
+
*
|
|
65
|
+
* @param entry - The event data
|
|
66
|
+
* @param databaseId - Optional named database
|
|
67
|
+
* @returns The created history entry, or null if recording failed
|
|
68
|
+
*/
|
|
69
|
+
export declare function recordHistory(entry: NewHistoryEntry, databaseId?: string): Promise<HistoryEntry | null>;
|
|
70
|
+
/**
|
|
71
|
+
* List the history of an object, newest first.
|
|
72
|
+
*
|
|
73
|
+
* Requires a composite index in the consuming project:
|
|
74
|
+
* objectCollection ASC, objectId ASC, at DESC on collection "_history".
|
|
75
|
+
*
|
|
76
|
+
* @param objectCollection - Collection of the object (e.g., "locations")
|
|
77
|
+
* @param objectId - Document ID of the object
|
|
78
|
+
* @param limit - Maximum entries to return (default: 100)
|
|
79
|
+
* @param databaseId - Optional named database
|
|
80
|
+
* @returns Array of history entries, newest first
|
|
81
|
+
*/
|
|
82
|
+
export declare function listHistory(objectCollection: string, objectId: string, limit?: number, databaseId?: string): Promise<HistoryEntry[]>;
|
|
83
|
+
//# sourceMappingURL=history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAIrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAY9C;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;CAMd,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,gBAAgB,EAAE,MAAM,CAAC;IACzB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,gDAAgD;IAChD,EAAE,EAAE,SAAS,CAAC;IACd,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAED,2DAA2D;AAC3D,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;AAS9D;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,eAAe,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAmBvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,eAAe,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAY9B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAAY,EACnB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,EAAE,CAAC,CAczB"}
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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"
|
|
13
|
+
};
|
|
14
|
+
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
|
+
}
|
|
31
|
+
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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
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
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { HistoryKind, addHistoryEntry, listHistory, recordHistory };
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export type { TrashItem } from './trash';
|
|
|
9
9
|
export { moveToTrash, restoreFromTrash, permanentDelete, listTrash, getTrashItem, } from './trash';
|
|
10
10
|
export type { AuthTrashOptions } from './auth-trash';
|
|
11
11
|
export { moveToTrashWithAuth, restoreFromTrashWithAuth, permanentDeleteWithAuth, } from './auth-trash';
|
|
12
|
+
export type { FieldChange, FieldChanges, ComputeChangesOptions, } from './changes';
|
|
13
|
+
export { computeChanges } from './changes';
|
|
14
|
+
export type { HistoryEntry, NewHistoryEntry } from './history';
|
|
15
|
+
export { HistoryKind, addHistoryEntry, recordHistory, listHistory, } from './history';
|
|
16
|
+
export type { MailObject, MailTemplate, MailMessage, MailAttachment, StoredMail, } from './mail';
|
|
17
|
+
export { addMailForSend, getMailById, getMailsByField } from './mail';
|
|
12
18
|
export { logger } from './logger';
|
|
13
19
|
export type { LogType, ScriptContextConfig, ScriptContext, } from './script-helpers';
|
|
14
20
|
export { scriptLog, initFirebaseAdmin, createScriptContext, } from './script-helpers';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAGrC,OAAO,EACL,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGtE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG3D,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAGpC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,YAAY,GACb,MAAM,SAAS,CAAC;AAGjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGlC,YAAY,EACV,OAAO,EACP,mBAAmB,EACnB,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAGrC,OAAO,EACL,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAGtE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG3D,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAGpC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,YAAY,GACb,MAAM,SAAS,CAAC;AAGjB,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EACL,WAAW,EACX,eAAe,EACf,aAAa,EACb,WAAW,GACZ,MAAM,WAAW,CAAC;AAGnB,YAAY,EACV,UAAU,EACV,YAAY,EACZ,WAAW,EACX,cAAc,EACd,UAAU,GACX,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAGtE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGlC,YAAY,EACV,OAAO,EACP,mBAAmB,EACnB,aAAa,GACd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,9 @@ export { getDb } from './firestore.js';
|
|
|
6
6
|
export { getNextCounter } from './counter.js';
|
|
7
7
|
export { getTrashItem, listTrash, moveToTrash, permanentDelete, restoreFromTrash } from './trash.js';
|
|
8
8
|
export { moveToTrashWithAuth, permanentDeleteWithAuth, restoreFromTrashWithAuth } from './auth-trash.js';
|
|
9
|
+
export { computeChanges } from './changes.js';
|
|
10
|
+
export { HistoryKind, addHistoryEntry, listHistory, recordHistory } from './history.js';
|
|
11
|
+
export { addMailForSend, getMailById, getMailsByField } from './mail.js';
|
|
9
12
|
export { logger } from './logger.js';
|
|
10
13
|
export { createScriptContext, initFirebaseAdmin, scriptLog } from './script-helpers.js';
|
|
11
14
|
export { runImportTemplates } from './import-templates.js';
|
package/dist/mail.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Timestamp } from 'firebase-admin/firestore';
|
|
2
|
+
export interface MailAttachment {
|
|
3
|
+
filename: string;
|
|
4
|
+
path: string;
|
|
5
|
+
contentType?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Template-based mail: the extension renders template `name` with `data`. */
|
|
8
|
+
export interface MailTemplate {
|
|
9
|
+
name: string;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
attachments?: MailAttachment[];
|
|
12
|
+
}
|
|
13
|
+
/** Direct mail content, used when no template is involved. */
|
|
14
|
+
export interface MailMessage {
|
|
15
|
+
subject: string;
|
|
16
|
+
html: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
attachments?: MailAttachment[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Core mail shape — the fields consumed by the Firebase mail extension.
|
|
22
|
+
* Apps extend this with their own correlation fields (e.g. eventId, counter)
|
|
23
|
+
* via the generic parameter of the mail functions.
|
|
24
|
+
*/
|
|
25
|
+
export interface MailObject {
|
|
26
|
+
to: string;
|
|
27
|
+
cc?: string;
|
|
28
|
+
bcc?: string;
|
|
29
|
+
replyTo?: string;
|
|
30
|
+
template?: MailTemplate;
|
|
31
|
+
message?: MailMessage;
|
|
32
|
+
}
|
|
33
|
+
/** A stored mail document, including extension-written delivery state. */
|
|
34
|
+
export type StoredMail<T extends MailObject = MailObject> = T & {
|
|
35
|
+
/** Auto-generated mail document ID */
|
|
36
|
+
id: string;
|
|
37
|
+
/** When the mail document was created (server-side) */
|
|
38
|
+
creation: Timestamp;
|
|
39
|
+
/** Delivery state, written by the mail extension */
|
|
40
|
+
delivery?: unknown;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Write a mail document for the Firebase mail extension to send.
|
|
44
|
+
*
|
|
45
|
+
* Throws on failure — the calling app decides whether a mail error aborts
|
|
46
|
+
* its flow or is just logged.
|
|
47
|
+
*
|
|
48
|
+
* @param mail - The mail data; app-specific extra fields are stored as-is
|
|
49
|
+
* @param collection - Mail collection name (default: "mail")
|
|
50
|
+
* @param databaseId - Optional named database
|
|
51
|
+
* @returns The created mail document ID
|
|
52
|
+
* @throws ValidationError if `to` is missing or neither template nor message is set
|
|
53
|
+
*/
|
|
54
|
+
export declare function addMailForSend<T extends MailObject>(mail: T, collection?: string, databaseId?: string): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Get a single mail document by ID (e.g. to inspect delivery state).
|
|
57
|
+
*
|
|
58
|
+
* @param mailId - The mail document ID
|
|
59
|
+
* @param collection - Mail collection name (default: "mail")
|
|
60
|
+
* @param databaseId - Optional named database
|
|
61
|
+
* @returns The stored mail or null if not found
|
|
62
|
+
*/
|
|
63
|
+
export declare function getMailById<T extends MailObject>(mailId: string, collection?: string, databaseId?: string): Promise<StoredMail<T> | null>;
|
|
64
|
+
/**
|
|
65
|
+
* List mail documents by an app-specific correlation field
|
|
66
|
+
* (e.g. field "eventId").
|
|
67
|
+
*
|
|
68
|
+
* Deliberately without orderBy — no composite index requirement per queried
|
|
69
|
+
* field; sorting is up to the caller.
|
|
70
|
+
*
|
|
71
|
+
* @param field - Field name to filter on
|
|
72
|
+
* @param value - Value the field must equal
|
|
73
|
+
* @param collection - Mail collection name (default: "mail")
|
|
74
|
+
* @param limit - Maximum documents to return (default: 100)
|
|
75
|
+
* @param databaseId - Optional named database
|
|
76
|
+
* @returns Array of stored mails
|
|
77
|
+
*/
|
|
78
|
+
export declare function getMailsByField<T extends MailObject>(field: string, value: unknown, collection?: string, limit?: number, databaseId?: string): Promise<StoredMail<T>[]>;
|
|
79
|
+
//# sourceMappingURL=mail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../src/mail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAcrD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;CAChC;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,0EAA0E;AAC1E,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IAAI,CAAC,GAAG;IAC9D,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,uDAAuD;IACvD,QAAQ,EAAE,SAAS,CAAC;IACpB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAMF;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,CAAC,SAAS,UAAU,EACvD,IAAI,EAAE,CAAC,EACP,UAAU,GAAE,MAAgC,EAC5C,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,CAAC,SAAS,UAAU,EACpD,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,MAAgC,EAC5C,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAY/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAC,CAAC,SAAS,UAAU,EACxD,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EACd,UAAU,GAAE,MAAgC,EAC5C,KAAK,GAAE,MAAY,EACnB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAe1B"}
|
package/dist/mail.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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";
|
|
6
|
+
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
|
+
}
|
|
20
|
+
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
|
+
};
|
|
30
|
+
}
|
|
31
|
+
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
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { addMailForSend, getMailById, getMailsByField };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@progressive-development/pd-provider-firebase-functions",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"clean:all": "rm -rf dist node_modules pnpm-lock.yaml",
|
|
40
40
|
"lint": "eslint --ext .ts src --ignore-path ../../.eslintignore && prettier \"**/*.ts\" --check --ignore-path ../../.eslintignore",
|
|
41
41
|
"format": "eslint --ext .ts src --fix --ignore-path ../../.eslintignore && prettier \"**/*.ts\" --write --ignore-path ../../.eslintignore",
|
|
42
|
-
"check": "pnpm run lint && pnpm run build"
|
|
42
|
+
"check": "pnpm run lint && pnpm run build",
|
|
43
|
+
"test": "vitest run"
|
|
43
44
|
}
|
|
44
45
|
}
|