@porulle/adapter-resend 0.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/README.md +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/package.json +49 -0
- package/src/index.ts +125 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @porulle/adapter-resend
|
|
2
|
+
|
|
3
|
+
Transactional email via [Resend](https://resend.com). Implements the `email.send` callback in `CommerceConfig`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineConfig } from "@porulle/core";
|
|
9
|
+
import { resendEmailAdapter } from "@porulle/adapter-resend";
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
email: {
|
|
13
|
+
send: resendEmailAdapter({
|
|
14
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
15
|
+
from: "Acme Store <orders@acme.com>",
|
|
16
|
+
// Optional: per-template subject + HTML overrides
|
|
17
|
+
subjects: {
|
|
18
|
+
"order-confirmation": (d) => `Your order ${d.orderNumber} is confirmed`,
|
|
19
|
+
},
|
|
20
|
+
templates: {
|
|
21
|
+
"order-confirmation": (d) => `<p>Thanks for ordering ${d.itemCount} items.</p>`,
|
|
22
|
+
},
|
|
23
|
+
// Optional: use Resend's server-side templates instead of local HTML
|
|
24
|
+
resendTemplateIds: {
|
|
25
|
+
"order-confirmation": "tmpl_abc123",
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
// …
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What gets sent
|
|
34
|
+
|
|
35
|
+
The kernel emits emails via the `email.send` callback for:
|
|
36
|
+
|
|
37
|
+
- `email-verification` — Better Auth signup
|
|
38
|
+
- `password-reset` — Better Auth password reset
|
|
39
|
+
- `order-confirmation` — `orders.afterCreate`
|
|
40
|
+
- (plugins can add their own — see plugin docs)
|
|
41
|
+
|
|
42
|
+
Templates not in `subjects` / `templates` get a minimal fallback so nothing silently fails to render.
|
|
43
|
+
|
|
44
|
+
## See also
|
|
45
|
+
|
|
46
|
+
- [Resend docs](https://resend.com/docs)
|
|
47
|
+
- `@porulle/adapter-ses` — AWS SES alternative
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface ResendAdapterOptions {
|
|
2
|
+
/** Resend API key (starts with re_). */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Default sender address (e.g., "Acme Store <orders@acme.com>"). */
|
|
5
|
+
from: string;
|
|
6
|
+
/**
|
|
7
|
+
* Maps template names to subject line generators.
|
|
8
|
+
* If a template is not in this map, the subject defaults to the template name.
|
|
9
|
+
*/
|
|
10
|
+
subjects?: Record<string, (data: Record<string, unknown>) => string>;
|
|
11
|
+
/**
|
|
12
|
+
* Maps template names to HTML body generators.
|
|
13
|
+
* If a template is not in this map, a minimal default is used.
|
|
14
|
+
*/
|
|
15
|
+
templates?: Record<string, (data: Record<string, unknown>) => string>;
|
|
16
|
+
/**
|
|
17
|
+
* Optional Resend template IDs. When provided, the adapter uses
|
|
18
|
+
* Resend's server-side template rendering instead of local HTML.
|
|
19
|
+
*/
|
|
20
|
+
resendTemplateIds?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates an email adapter backed by Resend.
|
|
24
|
+
*
|
|
25
|
+
* Implements the `config.email.send()` interface consumed by checkout hooks,
|
|
26
|
+
* auth (password reset, email verification), and appointment plugin notifications.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { resendAdapter } from "@porulle/adapter-resend";
|
|
31
|
+
*
|
|
32
|
+
* export default defineConfig({
|
|
33
|
+
* email: resendAdapter({
|
|
34
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
35
|
+
* from: "Acme Store <orders@acme.com>",
|
|
36
|
+
* }),
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function resendAdapter(options: ResendAdapterOptions): {
|
|
41
|
+
send(input: {
|
|
42
|
+
template: string;
|
|
43
|
+
to: string;
|
|
44
|
+
data?: Record<string, unknown>;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,CAAC;IACrE;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,CAAC;IACtE;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAgCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG;IAC5D,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9F,CAkDA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Resend } from "resend";
|
|
2
|
+
const DEFAULT_SUBJECTS = {
|
|
3
|
+
"order-confirmation": (d) => `Order Confirmed${d.orderId ? ` - #${String(d.orderId).slice(0, 8)}` : ""}`,
|
|
4
|
+
"order-status-change": (d) => `Order Update - ${d.newStatus ?? "Status Changed"}`,
|
|
5
|
+
"password-reset": () => "Reset Your Password",
|
|
6
|
+
"email-verification": () => "Verify Your Email Address",
|
|
7
|
+
"appointment:reminder": (d) => `Appointment Reminder${d.reminderType === "1h" ? " - Starting Soon" : ""}`,
|
|
8
|
+
"appointment:confirmation-notice": () => "Appointment Confirmed",
|
|
9
|
+
"appointment:cancellation-notice": () => "Appointment Cancelled",
|
|
10
|
+
"appointment:no-show-notice": () => "Missed Appointment",
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_TEMPLATES = {
|
|
13
|
+
"order-confirmation": (d) => `<h2>Order Confirmed</h2><p>Thank you for your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""}.</p>${d.total ? `<p>Total: ${d.currency ?? "USD"} ${String(d.total)}</p>` : ""}`,
|
|
14
|
+
"order-status-change": (d) => `<h2>Order Update</h2><p>Your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""} status has been updated to <strong>${d.newStatus ?? "unknown"}</strong>.</p>`,
|
|
15
|
+
"password-reset": (d) => `<h2>Reset Your Password</h2><p>Click the link below to reset your password:</p><p><a href="${d.url ?? "#"}">Reset Password</a></p>`,
|
|
16
|
+
"email-verification": (d) => `<h2>Verify Your Email</h2><p>Click the link below to verify your email address:</p><p><a href="${d.url ?? "#"}">Verify Email</a></p>`,
|
|
17
|
+
"appointment:reminder": (d) => `<h2>Appointment Reminder</h2><p>This is a${d.reminderType === "1h" ? " 1-hour" : " 24-hour"} reminder for your upcoming appointment.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
18
|
+
"appointment:confirmation-notice": (d) => `<h2>Appointment Confirmed</h2><p>Your appointment has been confirmed.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
19
|
+
"appointment:cancellation-notice": (d) => `<h2>Appointment Cancelled</h2><p>Your appointment has been cancelled.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
20
|
+
"appointment:no-show-notice": (d) => `<h2>Missed Appointment</h2><p>You missed your appointment. Please contact us to rebook.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Creates an email adapter backed by Resend.
|
|
24
|
+
*
|
|
25
|
+
* Implements the `config.email.send()` interface consumed by checkout hooks,
|
|
26
|
+
* auth (password reset, email verification), and appointment plugin notifications.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { resendAdapter } from "@porulle/adapter-resend";
|
|
31
|
+
*
|
|
32
|
+
* export default defineConfig({
|
|
33
|
+
* email: resendAdapter({
|
|
34
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
35
|
+
* from: "Acme Store <orders@acme.com>",
|
|
36
|
+
* }),
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function resendAdapter(options) {
|
|
41
|
+
const resend = new Resend(options.apiKey);
|
|
42
|
+
const subjects = { ...DEFAULT_SUBJECTS, ...options.subjects };
|
|
43
|
+
const templates = { ...DEFAULT_TEMPLATES, ...options.templates };
|
|
44
|
+
return {
|
|
45
|
+
async send(input) {
|
|
46
|
+
const data = input.data ?? {};
|
|
47
|
+
const subjectFn = subjects[input.template];
|
|
48
|
+
const subject = subjectFn ? subjectFn(data) : input.template;
|
|
49
|
+
// If a Resend template ID is configured, use server-side template rendering
|
|
50
|
+
const resendTemplateId = options.resendTemplateIds?.[input.template];
|
|
51
|
+
if (resendTemplateId) {
|
|
52
|
+
// Resend's template field is not yet typed in the SDK (upstream type gap).
|
|
53
|
+
// Cast required because CreateEmailOptions doesn't include `template`.
|
|
54
|
+
const { error: templateError } = await resend.emails.send({
|
|
55
|
+
from: options.from,
|
|
56
|
+
to: [input.to],
|
|
57
|
+
subject,
|
|
58
|
+
template: {
|
|
59
|
+
id: resendTemplateId,
|
|
60
|
+
variables: data,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
if (templateError) {
|
|
64
|
+
throw new Error(`Resend template email failed: ${templateError.message}`);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Otherwise, use local HTML template
|
|
69
|
+
const templateFn = templates[input.template];
|
|
70
|
+
const html = templateFn
|
|
71
|
+
? templateFn(data)
|
|
72
|
+
: `<p>Notification: ${input.template}</p><pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
73
|
+
const { error } = await resend.emails.send({
|
|
74
|
+
from: options.from,
|
|
75
|
+
to: [input.to],
|
|
76
|
+
subject,
|
|
77
|
+
html,
|
|
78
|
+
});
|
|
79
|
+
if (error) {
|
|
80
|
+
throw new Error(`Resend email failed: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@porulle/adapter-resend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
15
|
+
"check-types": "tsc --noEmit",
|
|
16
|
+
"lint": "eslint . --max-warnings 1000",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"resend": "^4.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@repo/eslint-config": "*",
|
|
24
|
+
"@repo/typescript-config": "*",
|
|
25
|
+
"@types/node": "^24.5.2",
|
|
26
|
+
"eslint": "^9.39.1",
|
|
27
|
+
"typescript": "5.9.2",
|
|
28
|
+
"vitest": "^3.2.4"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"description": "Transactional email via Resend. Implements the email.send callback in CommerceConfig.",
|
|
39
|
+
"homepage": "https://porulle-docs.vercel.app",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/asyncdotengineering/porulle/issues"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/asyncdotengineering/porulle.git",
|
|
46
|
+
"directory": "packages/adapters/adapter-resend"
|
|
47
|
+
},
|
|
48
|
+
"author": "Porulle contributors"
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Resend } from "resend";
|
|
2
|
+
|
|
3
|
+
export interface ResendAdapterOptions {
|
|
4
|
+
/** Resend API key (starts with re_). */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Default sender address (e.g., "Acme Store <orders@acme.com>"). */
|
|
7
|
+
from: string;
|
|
8
|
+
/**
|
|
9
|
+
* Maps template names to subject line generators.
|
|
10
|
+
* If a template is not in this map, the subject defaults to the template name.
|
|
11
|
+
*/
|
|
12
|
+
subjects?: Record<string, (data: Record<string, unknown>) => string>;
|
|
13
|
+
/**
|
|
14
|
+
* Maps template names to HTML body generators.
|
|
15
|
+
* If a template is not in this map, a minimal default is used.
|
|
16
|
+
*/
|
|
17
|
+
templates?: Record<string, (data: Record<string, unknown>) => string>;
|
|
18
|
+
/**
|
|
19
|
+
* Optional Resend template IDs. When provided, the adapter uses
|
|
20
|
+
* Resend's server-side template rendering instead of local HTML.
|
|
21
|
+
*/
|
|
22
|
+
resendTemplateIds?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SUBJECTS: Record<string, (data: Record<string, unknown>) => string> = {
|
|
26
|
+
"order-confirmation": (d) => `Order Confirmed${d.orderId ? ` - #${String(d.orderId).slice(0, 8)}` : ""}`,
|
|
27
|
+
"order-status-change": (d) => `Order Update - ${d.newStatus ?? "Status Changed"}`,
|
|
28
|
+
"password-reset": () => "Reset Your Password",
|
|
29
|
+
"email-verification": () => "Verify Your Email Address",
|
|
30
|
+
"appointment:reminder": (d) => `Appointment Reminder${d.reminderType === "1h" ? " - Starting Soon" : ""}`,
|
|
31
|
+
"appointment:confirmation-notice": () => "Appointment Confirmed",
|
|
32
|
+
"appointment:cancellation-notice": () => "Appointment Cancelled",
|
|
33
|
+
"appointment:no-show-notice": () => "Missed Appointment",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_TEMPLATES: Record<string, (data: Record<string, unknown>) => string> = {
|
|
37
|
+
"order-confirmation": (d) =>
|
|
38
|
+
`<h2>Order Confirmed</h2><p>Thank you for your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""}.</p>${d.total ? `<p>Total: ${d.currency ?? "USD"} ${String(d.total)}</p>` : ""}`,
|
|
39
|
+
"order-status-change": (d) =>
|
|
40
|
+
`<h2>Order Update</h2><p>Your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""} status has been updated to <strong>${d.newStatus ?? "unknown"}</strong>.</p>`,
|
|
41
|
+
"password-reset": (d) =>
|
|
42
|
+
`<h2>Reset Your Password</h2><p>Click the link below to reset your password:</p><p><a href="${d.url ?? "#"}">Reset Password</a></p>`,
|
|
43
|
+
"email-verification": (d) =>
|
|
44
|
+
`<h2>Verify Your Email</h2><p>Click the link below to verify your email address:</p><p><a href="${d.url ?? "#"}">Verify Email</a></p>`,
|
|
45
|
+
"appointment:reminder": (d) =>
|
|
46
|
+
`<h2>Appointment Reminder</h2><p>This is a${d.reminderType === "1h" ? " 1-hour" : " 24-hour"} reminder for your upcoming appointment.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
47
|
+
"appointment:confirmation-notice": (d) =>
|
|
48
|
+
`<h2>Appointment Confirmed</h2><p>Your appointment has been confirmed.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
49
|
+
"appointment:cancellation-notice": (d) =>
|
|
50
|
+
`<h2>Appointment Cancelled</h2><p>Your appointment has been cancelled.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
51
|
+
"appointment:no-show-notice": (d) =>
|
|
52
|
+
`<h2>Missed Appointment</h2><p>You missed your appointment. Please contact us to rebook.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates an email adapter backed by Resend.
|
|
57
|
+
*
|
|
58
|
+
* Implements the `config.email.send()` interface consumed by checkout hooks,
|
|
59
|
+
* auth (password reset, email verification), and appointment plugin notifications.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* import { resendAdapter } from "@porulle/adapter-resend";
|
|
64
|
+
*
|
|
65
|
+
* export default defineConfig({
|
|
66
|
+
* email: resendAdapter({
|
|
67
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
68
|
+
* from: "Acme Store <orders@acme.com>",
|
|
69
|
+
* }),
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function resendAdapter(options: ResendAdapterOptions): {
|
|
74
|
+
send(input: { template: string; to: string; data?: Record<string, unknown> }): Promise<void>;
|
|
75
|
+
} {
|
|
76
|
+
const resend = new Resend(options.apiKey);
|
|
77
|
+
|
|
78
|
+
const subjects = { ...DEFAULT_SUBJECTS, ...options.subjects };
|
|
79
|
+
const templates = { ...DEFAULT_TEMPLATES, ...options.templates };
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
async send(input) {
|
|
83
|
+
const data = input.data ?? {};
|
|
84
|
+
const subjectFn = subjects[input.template];
|
|
85
|
+
const subject = subjectFn ? subjectFn(data) : input.template;
|
|
86
|
+
|
|
87
|
+
// If a Resend template ID is configured, use server-side template rendering
|
|
88
|
+
const resendTemplateId = options.resendTemplateIds?.[input.template];
|
|
89
|
+
if (resendTemplateId) {
|
|
90
|
+
// Resend's template field is not yet typed in the SDK (upstream type gap).
|
|
91
|
+
// Cast required because CreateEmailOptions doesn't include `template`.
|
|
92
|
+
const { error: templateError } = await resend.emails.send({
|
|
93
|
+
from: options.from,
|
|
94
|
+
to: [input.to],
|
|
95
|
+
subject,
|
|
96
|
+
template: {
|
|
97
|
+
id: resendTemplateId,
|
|
98
|
+
variables: data as Record<string, string | number>,
|
|
99
|
+
},
|
|
100
|
+
} as unknown as Parameters<typeof resend.emails.send>[0]);
|
|
101
|
+
if (templateError) {
|
|
102
|
+
throw new Error(`Resend template email failed: ${templateError.message}`);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Otherwise, use local HTML template
|
|
108
|
+
const templateFn = templates[input.template];
|
|
109
|
+
const html = templateFn
|
|
110
|
+
? templateFn(data)
|
|
111
|
+
: `<p>Notification: ${input.template}</p><pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
112
|
+
|
|
113
|
+
const { error } = await resend.emails.send({
|
|
114
|
+
from: options.from,
|
|
115
|
+
to: [input.to],
|
|
116
|
+
subject,
|
|
117
|
+
html,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (error) {
|
|
121
|
+
throw new Error(`Resend email failed: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|