@forumone/throughline-email 0.0.1 → 0.2.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +133 -41
- package/dist/client.d.ts +78 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +63 -0
- package/dist/client.js.map +1 -0
- package/dist/functions/_shared.d.ts +17 -0
- package/dist/functions/_shared.d.ts.map +1 -0
- package/dist/functions/_shared.js +47 -0
- package/dist/functions/_shared.js.map +1 -0
- package/dist/functions/index.d.ts +7 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/functions/index.js +4 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/functions/notify-approval-decision.d.ts +23 -0
- package/dist/functions/notify-approval-decision.d.ts.map +1 -0
- package/dist/functions/notify-approval-decision.js +80 -0
- package/dist/functions/notify-approval-decision.js.map +1 -0
- package/dist/functions/notify-approval-expired.d.ts +21 -0
- package/dist/functions/notify-approval-expired.d.ts.map +1 -0
- package/dist/functions/notify-approval-expired.js +55 -0
- package/dist/functions/notify-approval-expired.js.map +1 -0
- package/dist/functions/notify-approval-request.d.ts +24 -0
- package/dist/functions/notify-approval-request.d.ts.map +1 -0
- package/dist/functions/notify-approval-request.js +93 -0
- package/dist/functions/notify-approval-request.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +79 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +39 -0
- package/dist/options.js.map +1 -0
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +76 -0
- package/dist/plugin.js.map +1 -0
- package/dist/templates/ApprovalDecision.d.ts +15 -0
- package/dist/templates/ApprovalDecision.d.ts.map +1 -0
- package/dist/templates/ApprovalDecision.js +56 -0
- package/dist/templates/ApprovalDecision.js.map +1 -0
- package/dist/templates/ApprovalExpired.d.ts +10 -0
- package/dist/templates/ApprovalExpired.d.ts.map +1 -0
- package/dist/templates/ApprovalExpired.js +8 -0
- package/dist/templates/ApprovalExpired.js.map +1 -0
- package/dist/templates/ApprovalRequest.d.ts +24 -0
- package/dist/templates/ApprovalRequest.d.ts.map +1 -0
- package/dist/templates/ApprovalRequest.js +58 -0
- package/dist/templates/ApprovalRequest.js.map +1 -0
- package/dist/templates/_layout.d.ts +17 -0
- package/dist/templates/_layout.d.ts.map +1 -0
- package/dist/templates/_layout.js +39 -0
- package/dist/templates/_layout.js.map +1 -0
- package/dist/templates/index.d.ts +9 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +5 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/tokens.d.ts +39 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +17 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +74 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @forumone/throughline-email
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fab82fc: Initial release of the email package. Pairs Resend with React Email templates and ships the three notification Inngest functions that close the approval workflow loop. Templates: `ApprovalRequestEmail` (preview + Approve / Request changes / Discuss), `ApprovalDecisionEmail` (granted / declined / changes-requested variants with decision-aware next-step copy), `ApprovalExpiredEmail`. All render to HTML and plaintext from the same React tree. Functions: `createNotifyApprovalRequestFunction` (subscribes to `notification/send-approval-request`, sends per approver in `notifiedApprovers` with each in its own `step.run`), `createNotifyApprovalDecisionFunction` (subscribes to `notification/send-approval-decision`), `createNotifyApprovalExpiredFunction` (subscribes to `approval/expired`). Themed via `EmailBrandTokens` (neutral defaults; brand name lands in header / From name / footer for consistency). Plugin exposes the client and functions via Symbols (`getEmailClient`, `getEmailFunctions`) for the client app's Inngest endpoint to compose. Throws at init if `RESEND_API_KEY` / `EMAIL_FROM_ADDRESS` / `resolveApprover` / `resolveRequester` / `buildActionUrl` are missing.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Forum One Communications Corporation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,45 +1,137 @@
|
|
|
1
1
|
# @forumone/throughline-email
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Transactional email for the Throughline framework. Pairs Resend with React Email templates that read brand tokens from the plugin options, and ships three Inngest functions that subscribe to the notification events the audit-event-echo workflow fires.
|
|
4
|
+
|
|
5
|
+
After this package, the approval workflow is end-to-end: a marketer requests approval via Claude, approvers receive a well-designed email with a one-sentence summary, a preview link, and three clear actions, and decisions / expirations route back to the requester automatically.
|
|
6
|
+
|
|
7
|
+
## What this package provides
|
|
8
|
+
|
|
9
|
+
- **`emailPlugin`** — registers an email client and the three notification Inngest functions on the Payload instance via Symbols.
|
|
10
|
+
- **`createEmailClient`** — Resend wrapper that lazy-imports both `resend` and `@react-email/render` and produces both HTML and plaintext from the same React tree on every send.
|
|
11
|
+
- **Three React Email templates** — `ApprovalRequestEmail`, `ApprovalDecisionEmail` (granted / declined / changes-requested variants), `ApprovalExpiredEmail`. Themed via `EmailBrandTokens`.
|
|
12
|
+
- **Three notification functions** — `createNotifyApprovalRequestFunction`, `createNotifyApprovalDecisionFunction`, `createNotifyApprovalExpiredFunction`.
|
|
13
|
+
- **Brand tokens** — neutral defaults (black on white, system sans, "Your Site"); deployments override via `emailPlugin({ tokens })`.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
4
16
|
|
|
5
|
-
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @forumone/throughline-email
|
|
19
|
+
```
|
|
6
20
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
21
|
+
Peers: `payload@^3.0.0`, `inngest@^4.0.0`, `react@^18 || ^19`. The plugin lazy-loads `resend` and `@react-email/render` on first send, so a misconfigured deploy fails on first email rather than at boot.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
In your Payload config:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { buildConfig } from 'payload'
|
|
29
|
+
import { auditPlugin, createInngestClient } from '@forumone/throughline-core'
|
|
30
|
+
import { emailPlugin } from '@forumone/throughline-email'
|
|
31
|
+
|
|
32
|
+
const inngest = createInngestClient({ id: 'my-site' })
|
|
33
|
+
|
|
34
|
+
export default buildConfig({
|
|
35
|
+
// collections, db, secret...
|
|
36
|
+
plugins: [
|
|
37
|
+
auditPlugin({ inngest }),
|
|
38
|
+
emailPlugin({
|
|
39
|
+
inngest,
|
|
40
|
+
tokens: { brandName: 'Acme Foundation', brandPrimary: '#5B21B6' },
|
|
41
|
+
resolveApprover: async (userId) => {
|
|
42
|
+
const user = await payload.findByID({ collection: 'users', id: userId })
|
|
43
|
+
return user ? { email: user.email, name: user.name } : null
|
|
44
|
+
},
|
|
45
|
+
resolveRequester: async (userId) => {
|
|
46
|
+
const user = await payload.findByID({ collection: 'users', id: userId })
|
|
47
|
+
return user ? { email: user.email, name: user.name } : null
|
|
48
|
+
},
|
|
49
|
+
buildActionUrl: async ({ approvalId, action, approverId }) => {
|
|
50
|
+
// Wrap your approvals plugin's HMAC token. The action endpoint
|
|
51
|
+
// confirms-on-first-hit, so any well-formed URL works here.
|
|
52
|
+
return `${process.env.NEXT_PUBLIC_SERVER_URL}/api/approvals/action?token=...`
|
|
53
|
+
},
|
|
54
|
+
// Optional preview URL builder for cases where the approval record
|
|
55
|
+
// doesn't carry a previewUrl directly.
|
|
56
|
+
buildPreviewUrl: async ({ approvalId }) =>
|
|
57
|
+
`${process.env.NEXT_PUBLIC_SERVER_URL}/preview?approval=${approvalId}`,
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then in your client app's Inngest endpoint:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// src/app/api/inngest/route.ts
|
|
67
|
+
import { serve } from 'inngest/next'
|
|
68
|
+
import { getPayload } from 'payload'
|
|
69
|
+
import config from '@payload-config'
|
|
70
|
+
import { inngest } from '@/lib/inngest'
|
|
71
|
+
import { getEmailFunctions } from '@forumone/throughline-email'
|
|
72
|
+
|
|
73
|
+
const payload = await getPayload({ config })
|
|
74
|
+
const emailFunctions = getEmailFunctions(payload)
|
|
75
|
+
|
|
76
|
+
export const { GET, POST, PUT } = serve({
|
|
77
|
+
client: inngest,
|
|
78
|
+
functions: [...emailFunctions /* plus your other functions */],
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The plugin registers functions into the Payload instance via Symbol; the endpoint reads them via `getEmailFunctions`. Same shape used by `@forumone/throughline-integrations`.
|
|
83
|
+
|
|
84
|
+
## How the events flow
|
|
85
|
+
|
|
86
|
+
1. Approvals server fires `approval.requested` / `approval.granted` / `approval.declined` / `approval.changes_requested` audit events.
|
|
87
|
+
2. C10's `audit-event-echo` re-fires these as `notification/send-approval-request` and `notification/send-approval-decision`.
|
|
88
|
+
3. This package's notification functions handle those events and email the right people.
|
|
89
|
+
|
|
90
|
+
The `approval/expired` event (fired by C10's `expire-stale-approvals` cron) is handled directly — no audit-echo translation is needed.
|
|
91
|
+
|
|
92
|
+
## Brand tokens
|
|
93
|
+
|
|
94
|
+
Tokens are merged onto neutral defaults; pass only what you want to override:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
emailPlugin({
|
|
98
|
+
tokens: {
|
|
99
|
+
brandName: 'Acme Foundation', // header + From name + footer
|
|
100
|
+
brandPrimary: '#5B21B6', // approve button + discuss link
|
|
101
|
+
fontFamilySans: '"Roobert", system-ui, sans-serif',
|
|
102
|
+
},
|
|
103
|
+
// ...
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`brandName` lands in three places (layout header, From display name, footer disclaimer) so "this came from Acme Foundation" stays consistent.
|
|
108
|
+
|
|
109
|
+
## Why per-recipient `step.run`
|
|
110
|
+
|
|
111
|
+
The approval-request notifier sends to every approver in `notifiedApprovers`. Each recipient is wrapped in its own `step.run`, so a bouncing inbox retries without re-sending to the others. Inngest's idempotency keys are per-step, not per-function — batching all sends in one step would mean every retry re-emails everyone.
|
|
112
|
+
|
|
113
|
+
## Plaintext is non-optional
|
|
114
|
+
|
|
115
|
+
Every email renders to both HTML and plaintext from the same React tree (React Email's `render(..., { plainText: true })`). Plaintext is required for accessibility (screen readers), deliverability (spam scores improve dramatically), and for clients that refuse HTML.
|
|
116
|
+
|
|
117
|
+
## Options reference
|
|
118
|
+
|
|
119
|
+
| Option | Default | Notes |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `inngest` | required | Fires no events itself; needed to register the three functions |
|
|
122
|
+
| `apiKey` | `RESEND_API_KEY` env | Throws at init if neither is set |
|
|
123
|
+
| `fromAddress` | `EMAIL_FROM_ADDRESS` env | Required |
|
|
124
|
+
| `fromName` | `EMAIL_FROM_NAME` env → `tokens.brandName` → `'Your Site'` | |
|
|
125
|
+
| `replyTo` | `EMAIL_REPLY_TO` env | Optional |
|
|
126
|
+
| `tokens` | `defaultTokens` (merged onto) | Partial override |
|
|
127
|
+
| `approvalsCollectionSlug` | `'approvals'` | Match what your approvals plugin uses |
|
|
128
|
+
| `resolveApprover` | required | `(userId) => {email, name}` |
|
|
129
|
+
| `resolveRequester` | required | `(userId) => {email, name}` |
|
|
130
|
+
| `buildActionUrl` | required | Per-action URL builder; wrap your approvals plugin's HMAC token |
|
|
131
|
+
| `buildPreviewUrl` | optional | Used only when the approval record has no `previewUrl` |
|
|
132
|
+
|
|
133
|
+
## Related packages
|
|
134
|
+
|
|
135
|
+
- `@forumone/throughline-core` — required peer; provides logger and audit infrastructure
|
|
136
|
+
- `@forumone/throughline-approvals` — owns the approvals collection this package reads
|
|
137
|
+
- `@forumone/throughline-workflows` — `audit-event-echo` is the upstream fan-out; `expire-stale-approvals` fires the expiration event this package subscribes to
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
export interface EmailClient {
|
|
3
|
+
send(params: SendEmailParams): Promise<SendEmailResult>;
|
|
4
|
+
}
|
|
5
|
+
export interface SendEmailParams {
|
|
6
|
+
to: string | string[];
|
|
7
|
+
subject: string;
|
|
8
|
+
template: ReactElement;
|
|
9
|
+
tags?: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}>;
|
|
13
|
+
replyTo?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SendEmailResult {
|
|
16
|
+
id: string;
|
|
17
|
+
deliveredAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface EmailClientOptions {
|
|
20
|
+
apiKey: string;
|
|
21
|
+
fromAddress: string;
|
|
22
|
+
fromName: string;
|
|
23
|
+
defaultReplyTo?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Optional Resend constructor override for tests or self-hosted Resend
|
|
26
|
+
* deployments. The default loads `resend` lazily so the package is
|
|
27
|
+
* still importable in environments without Resend installed (e.g. tests
|
|
28
|
+
* that mock the client entirely).
|
|
29
|
+
*/
|
|
30
|
+
resendClient?: {
|
|
31
|
+
emails: {
|
|
32
|
+
send: (input: ResendSendInput) => Promise<ResendSendResult>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Optional renderer override. The default lazily imports
|
|
37
|
+
* `@react-email/render` and produces both HTML and plaintext.
|
|
38
|
+
*/
|
|
39
|
+
render?: TemplateRenderer;
|
|
40
|
+
}
|
|
41
|
+
export interface TemplateRenderer {
|
|
42
|
+
toHtml: (template: ReactElement) => Promise<string>;
|
|
43
|
+
toText: (template: ReactElement) => Promise<string>;
|
|
44
|
+
}
|
|
45
|
+
interface ResendSendInput {
|
|
46
|
+
from: string;
|
|
47
|
+
to: string[];
|
|
48
|
+
subject: string;
|
|
49
|
+
html: string;
|
|
50
|
+
text: string;
|
|
51
|
+
tags?: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
value: string;
|
|
54
|
+
}>;
|
|
55
|
+
replyTo?: string;
|
|
56
|
+
}
|
|
57
|
+
interface ResendSendResult {
|
|
58
|
+
data: {
|
|
59
|
+
id: string;
|
|
60
|
+
} | null;
|
|
61
|
+
error: {
|
|
62
|
+
message: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
} | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wraps Resend with React Email rendering and a stable result envelope.
|
|
68
|
+
* Each `send` call produces both HTML and plaintext from the same React
|
|
69
|
+
* tree — plaintext is non-optional for accessibility (screen readers),
|
|
70
|
+
* deliverability (spam scores), and clients that refuse HTML.
|
|
71
|
+
*
|
|
72
|
+
* Construction is lazy: the Resend SDK and the renderer are only loaded
|
|
73
|
+
* when `send()` is first called. That keeps tests that mock the client
|
|
74
|
+
* fully runnable in environments where neither package is installed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createEmailClient(options: EmailClientOptions): EmailClient;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAEzC,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;CACxD;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,YAAY,CAAA;IACtB,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE;QAAE,MAAM,EAAE;YAAE,IAAI,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAA;SAAE,CAAA;KAAE,CAAA;IAC1F;;;OAGG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACnD,MAAM,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CACpD;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,EAAE,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,UAAU,gBAAgB;IACxB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC3B,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CACjD;AAQD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAuD1E"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps Resend with React Email rendering and a stable result envelope.
|
|
3
|
+
* Each `send` call produces both HTML and plaintext from the same React
|
|
4
|
+
* tree — plaintext is non-optional for accessibility (screen readers),
|
|
5
|
+
* deliverability (spam scores), and clients that refuse HTML.
|
|
6
|
+
*
|
|
7
|
+
* Construction is lazy: the Resend SDK and the renderer are only loaded
|
|
8
|
+
* when `send()` is first called. That keeps tests that mock the client
|
|
9
|
+
* fully runnable in environments where neither package is installed.
|
|
10
|
+
*/
|
|
11
|
+
export function createEmailClient(options) {
|
|
12
|
+
const from = `${options.fromName} <${options.fromAddress}>`;
|
|
13
|
+
let cachedResend = options.resendClient;
|
|
14
|
+
let cachedRenderer = options.render;
|
|
15
|
+
async function getResend() {
|
|
16
|
+
if (cachedResend)
|
|
17
|
+
return cachedResend;
|
|
18
|
+
const mod = (await import('resend'));
|
|
19
|
+
cachedResend = new mod.Resend(options.apiKey);
|
|
20
|
+
return cachedResend;
|
|
21
|
+
}
|
|
22
|
+
async function getRenderer() {
|
|
23
|
+
if (cachedRenderer)
|
|
24
|
+
return cachedRenderer;
|
|
25
|
+
const mod = (await import('@react-email/render'));
|
|
26
|
+
cachedRenderer = {
|
|
27
|
+
toHtml: (template) => mod.render(template),
|
|
28
|
+
toText: (template) => mod.render(template, { plainText: true }),
|
|
29
|
+
};
|
|
30
|
+
return cachedRenderer;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
async send(params) {
|
|
34
|
+
const renderer = await getRenderer();
|
|
35
|
+
const [html, text] = await Promise.all([
|
|
36
|
+
renderer.toHtml(params.template),
|
|
37
|
+
renderer.toText(params.template),
|
|
38
|
+
]);
|
|
39
|
+
const resend = await getResend();
|
|
40
|
+
const input = {
|
|
41
|
+
from,
|
|
42
|
+
to: Array.isArray(params.to) ? params.to : [params.to],
|
|
43
|
+
subject: params.subject,
|
|
44
|
+
html,
|
|
45
|
+
text,
|
|
46
|
+
};
|
|
47
|
+
if (params.tags)
|
|
48
|
+
input.tags = params.tags;
|
|
49
|
+
const replyTo = params.replyTo ?? options.defaultReplyTo;
|
|
50
|
+
if (replyTo)
|
|
51
|
+
input.replyTo = replyTo;
|
|
52
|
+
const result = await resend.emails.send(input);
|
|
53
|
+
if (result.error) {
|
|
54
|
+
throw new Error(`Email send failed: ${result.error.message}`);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
id: result.data?.id ?? 'unknown',
|
|
58
|
+
deliveredAt: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAgEA;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,IAAI,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,WAAW,GAAG,CAAA;IAE3D,IAAI,YAAY,GAAmD,OAAO,CAAC,YAAY,CAAA;IACvF,IAAI,cAAc,GAAiC,OAAO,CAAC,MAAM,CAAA;IAEjE,KAAK,UAAU,SAAS;QACtB,IAAI,YAAY;YAAE,OAAO,YAAY,CAAA;QACrC,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAA2B,CAAA;QAC9D,YAAY,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAC7C,OAAO,YAAY,CAAA;IACrB,CAAC;IAED,KAAK,UAAU,WAAW;QACxB,IAAI,cAAc;YAAE,OAAO,cAAc,CAAA;QACzC,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAE/C,CAAA;QACD,cAAc,GAAG;YACf,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC;YAC1C,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;SAChE,CAAA;QACD,OAAO,cAAc,CAAA;IACvB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,MAAM;YACf,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAA;YACpC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACrC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;gBAChC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;aACjC,CAAC,CAAA;YAEF,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAA;YAChC,MAAM,KAAK,GAAoB;gBAC7B,IAAI;gBACJ,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtD,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI;gBACJ,IAAI;aACL,CAAA;YACD,IAAI,MAAM,CAAC,IAAI;gBAAE,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;YACzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,cAAc,CAAA;YACxD,IAAI,OAAO;gBAAE,KAAK,CAAC,OAAO,GAAG,OAAO,CAAA;YAEpC,MAAM,MAAM,GAAG,MAAM,MAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAC/C,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,sBAAsB,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;YAC/D,CAAC;YACD,OAAO;gBACL,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,SAAS;gBAChC,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACtC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApprovalTargetKind } from '../templates/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Maps a collection slug to the human-friendly noun the templates render.
|
|
4
|
+
* Defaults to "item" so unknown collections still produce sensible copy
|
|
5
|
+
* ("the item Homepage" reads weirder than "the page Homepage" but it's
|
|
6
|
+
* better than dropping the noun entirely).
|
|
7
|
+
*/
|
|
8
|
+
export declare function targetKindFromCollection(slug: string): ApprovalTargetKind;
|
|
9
|
+
export declare function unwrapRelationshipId(value: unknown): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Defensive normalizer for the `notifiedApprovers` json field. Older
|
|
12
|
+
* snapshots stored an array of strings; the post-C7 fixup wraps each
|
|
13
|
+
* as `{ id, at, channel }`. We accept either shape.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readApproverIds(value: unknown): string[];
|
|
16
|
+
export declare function formatHumanDate(iso: string): string;
|
|
17
|
+
//# sourceMappingURL=_shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../src/functions/_shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAO/D;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAEzE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMlE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAWxD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAInD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const COLLECTION_TO_KIND = {
|
|
2
|
+
pages: 'page',
|
|
3
|
+
posts: 'post',
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Maps a collection slug to the human-friendly noun the templates render.
|
|
7
|
+
* Defaults to "item" so unknown collections still produce sensible copy
|
|
8
|
+
* ("the item Homepage" reads weirder than "the page Homepage" but it's
|
|
9
|
+
* better than dropping the noun entirely).
|
|
10
|
+
*/
|
|
11
|
+
export function targetKindFromCollection(slug) {
|
|
12
|
+
return COLLECTION_TO_KIND[slug] ?? 'item';
|
|
13
|
+
}
|
|
14
|
+
export function unwrapRelationshipId(value) {
|
|
15
|
+
if (typeof value === 'string')
|
|
16
|
+
return value;
|
|
17
|
+
if (value && typeof value === 'object' && 'id' in value) {
|
|
18
|
+
return String(value.id);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Defensive normalizer for the `notifiedApprovers` json field. Older
|
|
24
|
+
* snapshots stored an array of strings; the post-C7 fixup wraps each
|
|
25
|
+
* as `{ id, at, channel }`. We accept either shape.
|
|
26
|
+
*/
|
|
27
|
+
export function readApproverIds(value) {
|
|
28
|
+
if (!Array.isArray(value))
|
|
29
|
+
return [];
|
|
30
|
+
const ids = [];
|
|
31
|
+
for (const entry of value) {
|
|
32
|
+
if (typeof entry === 'string') {
|
|
33
|
+
ids.push(entry);
|
|
34
|
+
}
|
|
35
|
+
else if (entry && typeof entry === 'object' && 'id' in entry) {
|
|
36
|
+
ids.push(String(entry.id));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ids;
|
|
40
|
+
}
|
|
41
|
+
export function formatHumanDate(iso) {
|
|
42
|
+
const date = new Date(iso);
|
|
43
|
+
if (Number.isNaN(date.getTime()))
|
|
44
|
+
return iso;
|
|
45
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=_shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_shared.js","sourceRoot":"","sources":["../../src/functions/_shared.ts"],"names":[],"mappings":"AAEA,MAAM,kBAAkB,GAAuC;IAC7D,KAAK,EAAE,MAAM;IACb,KAAK,EAAE,MAAM;CACd,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,MAAM,CAAA;AAC3C,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC;QACxD,OAAO,MAAM,CAAE,KAAyB,CAAC,EAAE,CAAC,CAAA;IAC9C,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACpC,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjB,CAAC;aAAM,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC;YAC/D,GAAG,CAAC,IAAI,CAAC,MAAM,CAAE,KAAyB,CAAC,EAAE,CAAC,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1B,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,GAAG,CAAA;IAC5C,OAAO,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;AAC9F,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createNotifyApprovalRequestFunction } from './notify-approval-request.js';
|
|
2
|
+
export type { NotifyApprovalRequestDeps } from './notify-approval-request.js';
|
|
3
|
+
export { createNotifyApprovalDecisionFunction } from './notify-approval-decision.js';
|
|
4
|
+
export type { NotifyApprovalDecisionDeps } from './notify-approval-decision.js';
|
|
5
|
+
export { createNotifyApprovalExpiredFunction } from './notify-approval-expired.js';
|
|
6
|
+
export type { NotifyApprovalExpiredDeps } from './notify-approval-expired.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/functions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAA;AAClF,YAAY,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAA;AAC7E,OAAO,EAAE,oCAAoC,EAAE,MAAM,+BAA+B,CAAA;AACpF,YAAY,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAA;AAC/E,OAAO,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAA;AAClF,YAAY,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createNotifyApprovalRequestFunction } from './notify-approval-request.js';
|
|
2
|
+
export { createNotifyApprovalDecisionFunction } from './notify-approval-decision.js';
|
|
3
|
+
export { createNotifyApprovalExpiredFunction } from './notify-approval-expired.js';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/functions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAA;AAElF,OAAO,EAAE,oCAAoC,EAAE,MAAM,+BAA+B,CAAA;AAEpF,OAAO,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Inngest, InngestFunction } from 'inngest';
|
|
2
|
+
import type { Payload } from 'payload';
|
|
3
|
+
import type { EmailClient } from '../client.js';
|
|
4
|
+
import type { EmailBrandTokens } from '../tokens.js';
|
|
5
|
+
import { type EmailPluginOptions } from '../options.js';
|
|
6
|
+
export interface NotifyApprovalDecisionDeps {
|
|
7
|
+
inngest: Inngest;
|
|
8
|
+
payload: Payload;
|
|
9
|
+
client: EmailClient;
|
|
10
|
+
tokens: EmailBrandTokens;
|
|
11
|
+
options: EmailPluginOptions;
|
|
12
|
+
/** Override the function id. Default: `notify-approval-decision`. */
|
|
13
|
+
id?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Subscribes to `notification/send-approval-decision` (fired by the C10
|
|
17
|
+
* audit-event-echo when one of `approval.granted`, `approval.declined`,
|
|
18
|
+
* or `approval.changes_requested` is recorded). The decider's audit
|
|
19
|
+
* action travels with the event so we render the right variant without
|
|
20
|
+
* re-checking the approval's status.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createNotifyApprovalDecisionFunction(deps: NotifyApprovalDecisionDeps): InngestFunction.Any;
|
|
23
|
+
//# sourceMappingURL=notify-approval-decision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify-approval-decision.d.ts","sourceRoot":"","sources":["../../src/functions/notify-approval-decision.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAKpD,OAAO,EAAqC,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAG1F,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,WAAW,CAAA;IACnB,MAAM,EAAE,gBAAgB,CAAA;IACxB,OAAO,EAAE,kBAAkB,CAAA;IAC3B,qEAAqE;IACrE,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAcD;;;;;;GAMG;AACH,wBAAgB,oCAAoC,CAClD,IAAI,EAAE,0BAA0B,GAC/B,eAAe,CAAC,GAAG,CAyErB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ApprovalDecisionEmail, } from '../templates/index.js';
|
|
2
|
+
import { DEFAULT_APPROVALS_COLLECTION_SLUG } from '../options.js';
|
|
3
|
+
import { unwrapRelationshipId } from './_shared.js';
|
|
4
|
+
const DECISION_BY_ACTION = {
|
|
5
|
+
'approval.granted': 'granted',
|
|
6
|
+
'approval.declined': 'declined',
|
|
7
|
+
'approval.changes_requested': 'changes-requested',
|
|
8
|
+
};
|
|
9
|
+
const SUBJECT_BY_DECISION = {
|
|
10
|
+
granted: (title) => `Approved: ${title}`,
|
|
11
|
+
declined: (title) => `Not approved: ${title}`,
|
|
12
|
+
'changes-requested': (title) => `Changes requested on ${title}`,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Subscribes to `notification/send-approval-decision` (fired by the C10
|
|
16
|
+
* audit-event-echo when one of `approval.granted`, `approval.declined`,
|
|
17
|
+
* or `approval.changes_requested` is recorded). The decider's audit
|
|
18
|
+
* action travels with the event so we render the right variant without
|
|
19
|
+
* re-checking the approval's status.
|
|
20
|
+
*/
|
|
21
|
+
export function createNotifyApprovalDecisionFunction(deps) {
|
|
22
|
+
return deps.inngest.createFunction({
|
|
23
|
+
id: deps.id ?? 'notify-approval-decision',
|
|
24
|
+
retries: 3,
|
|
25
|
+
triggers: [{ event: 'notification/send-approval-decision' }],
|
|
26
|
+
}, async ({ event, step, logger }) => {
|
|
27
|
+
const data = (event.data ?? {});
|
|
28
|
+
const approvalId = data.approvalId;
|
|
29
|
+
const decisionAction = data.decision ?? '';
|
|
30
|
+
if (!approvalId)
|
|
31
|
+
return { skipped: true, reason: 'no-approvalId' };
|
|
32
|
+
const decision = DECISION_BY_ACTION[decisionAction];
|
|
33
|
+
if (!decision) {
|
|
34
|
+
logger.warn('Unknown decision action; skipping', { approvalId, decisionAction });
|
|
35
|
+
return { skipped: true, reason: 'unknown-decision' };
|
|
36
|
+
}
|
|
37
|
+
const collectionSlug = deps.options.approvalsCollectionSlug ?? DEFAULT_APPROVALS_COLLECTION_SLUG;
|
|
38
|
+
const approval = await step.run('load-approval', async () => deps.payload.findByID({ collection: collectionSlug, id: approvalId, depth: 1 }));
|
|
39
|
+
if (!approval)
|
|
40
|
+
return { skipped: true, reason: 'approval-not-found' };
|
|
41
|
+
const requesterId = unwrapRelationshipId(approval['requestedBy']);
|
|
42
|
+
if (!requesterId) {
|
|
43
|
+
logger.warn('Approval has no requester; cannot notify', { approvalId });
|
|
44
|
+
return { skipped: true, reason: 'no-requester' };
|
|
45
|
+
}
|
|
46
|
+
const targetTitle = String(approval['targetTitle'] ?? '(untitled)');
|
|
47
|
+
const decisionNotes = typeof approval['decisionNotes'] === 'string' ? approval['decisionNotes'] : undefined;
|
|
48
|
+
const previewUrl = typeof approval['previewUrl'] === 'string' ? approval['previewUrl'] : '';
|
|
49
|
+
const decidedById = unwrapRelationshipId(approval['decidedBy']);
|
|
50
|
+
const decidedByRecord = decidedById ? await deps.options.resolveApprover(decidedById) : null;
|
|
51
|
+
const decidedByName = decidedByRecord?.name ?? decidedByRecord?.email ?? 'a reviewer';
|
|
52
|
+
await step.run(`send-to-${requesterId}`, async () => {
|
|
53
|
+
const requester = await deps.options.resolveRequester(requesterId);
|
|
54
|
+
if (!requester) {
|
|
55
|
+
logger.warn('No email for requester; skipping', { requesterId, approvalId });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await deps.client.send({
|
|
59
|
+
to: requester.email,
|
|
60
|
+
subject: SUBJECT_BY_DECISION[decision](targetTitle),
|
|
61
|
+
template: ApprovalDecisionEmail({
|
|
62
|
+
requesterName: requester.name ?? requester.email,
|
|
63
|
+
decidedBy: decidedByName,
|
|
64
|
+
targetTitle,
|
|
65
|
+
decision,
|
|
66
|
+
...(decisionNotes ? { decisionNotes } : {}),
|
|
67
|
+
previewUrl,
|
|
68
|
+
tokens: deps.tokens,
|
|
69
|
+
}),
|
|
70
|
+
tags: [
|
|
71
|
+
{ name: 'type', value: 'approval-decision' },
|
|
72
|
+
{ name: 'approval-id', value: approvalId },
|
|
73
|
+
{ name: 'decision', value: decision },
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
return { approvalId, decision, sent: 1 };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=notify-approval-decision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify-approval-decision.js","sourceRoot":"","sources":["../../src/functions/notify-approval-decision.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Inngest, InngestFunction } from 'inngest';
|
|
2
|
+
import type { Payload } from 'payload';
|
|
3
|
+
import type { EmailClient } from '../client.js';
|
|
4
|
+
import type { EmailBrandTokens } from '../tokens.js';
|
|
5
|
+
import { type EmailPluginOptions } from '../options.js';
|
|
6
|
+
export interface NotifyApprovalExpiredDeps {
|
|
7
|
+
inngest: Inngest;
|
|
8
|
+
payload: Payload;
|
|
9
|
+
client: EmailClient;
|
|
10
|
+
tokens: EmailBrandTokens;
|
|
11
|
+
options: EmailPluginOptions;
|
|
12
|
+
/** Override the function id. Default: `notify-approval-expired`. */
|
|
13
|
+
id?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Subscribes to `approval/expired` (fired by the C10 expire-stale-approvals
|
|
17
|
+
* cron). Notifies the original requester so they can decide whether to
|
|
18
|
+
* re-request approval or drop the change.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createNotifyApprovalExpiredFunction(deps: NotifyApprovalExpiredDeps): InngestFunction.Any;
|
|
21
|
+
//# sourceMappingURL=notify-approval-expired.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify-approval-expired.d.ts","sourceRoot":"","sources":["../../src/functions/notify-approval-expired.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEpD,OAAO,EAAqC,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAG1F,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,WAAW,CAAA;IACnB,MAAM,EAAE,gBAAgB,CAAA;IACxB,OAAO,EAAE,kBAAkB,CAAA;IAC3B,oEAAoE;IACpE,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CACjD,IAAI,EAAE,yBAAyB,GAC9B,eAAe,CAAC,GAAG,CA0DrB"}
|