@pilely/simple-email 0.3.0 → 0.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/README.md +70 -78
- package/dist/api.d.ts +95 -0
- package/dist/api.js +192 -0
- package/dist/hooks.d.ts +46 -0
- package/dist/hooks.js +109 -0
- package/dist/index.d.ts +3 -95
- package/dist/index.js +4 -190
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,98 +1,90 @@
|
|
|
1
1
|
# `@pilely/simple-email`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
React hooks for the `simple_email` service — the platform's managed transactional email. A query hook for every read, a mutation hook for every write. Render them inside `@pilely/core`'s `<PilelyProvider>`.
|
|
4
|
+
|
|
5
|
+
## Hooks
|
|
6
|
+
|
|
7
|
+
| Hook | Wraps | Kind |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `useSend()` | `/send` | mutation, `{ to, subject, html }` or `{ to, template_id, variables? }` |
|
|
10
|
+
| `useSends({ limit? })` | `/sends/list`, paged | infinite query; this app's outbox, `SendRow[]` |
|
|
11
|
+
| `useEmailAccount()` | `accounts/{app}/info` | query; this app's account with its daily cap, sent today, credit and phone state |
|
|
12
|
+
| `useEmailAccounts({ limit? })` | `accounts/list`, paged | infinite query; every account the caller owns |
|
|
13
|
+
| `useCreateEmailAccount()` | `accounts/create` | mutation, the optional send group |
|
|
14
|
+
| `useDeleteEmailAccount()` | `accounts/{app}/delete` | mutation, no variables |
|
|
15
|
+
| `useSetEmailAccountAccess()` | `accounts/{app}/access/set` | mutation, the send group (`null` = owner-only) |
|
|
16
|
+
| `useTemplates({ limit? })` | `templates/list`, paged | infinite query; metadata only |
|
|
17
|
+
| `useTemplate(templateId)` | `templates/{t}/info` | query; the template with its `html`; an `undefined` id disables it |
|
|
18
|
+
| `useCreateTemplate()` | `templates/create` | mutation, `{ name, subject, html }` (`name` may be `null`) |
|
|
19
|
+
| `useUpdateTemplate()` | `templates/{t}/update` | mutation, `{ templateId, subject, html }` — a full replace |
|
|
20
|
+
| `useDeleteTemplate()` | `templates/{t}/delete` | mutation, the template id |
|
|
21
|
+
|
|
22
|
+
The app id is added from `@pilely/core` on every route; it is never a variable. Every write waits for the server: an email "sent" before the server accepted it would be a lie.
|
|
23
|
+
|
|
24
|
+
## What each write refreshes
|
|
25
|
+
|
|
26
|
+
| Write | Invalidates |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| send | `useSends()` |
|
|
29
|
+
| account create / set access | `useEmailAccounts()`, `useEmailAccount()` |
|
|
30
|
+
| account delete | `useEmailAccounts()`, `useEmailAccount()`, and the templates under it |
|
|
31
|
+
| template create | `useTemplates()` |
|
|
32
|
+
| template update / delete on `t` | `useTemplates()`, `useTemplate(t)` |
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useSend, useSends } from "@pilely/simple-email";
|
|
38
|
+
|
|
39
|
+
export function Outbox() {
|
|
40
|
+
const { data: sends } = useSends();
|
|
41
|
+
const send = useSend();
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<button onClick={() => send.mutate({ to: ["friend@example.com"], subject: "Hi", html: "<p>Hello</p>" })}>
|
|
45
|
+
Send
|
|
46
|
+
</button>
|
|
47
|
+
{send.error && <p role="alert">{send.error.message}</p>}
|
|
48
|
+
<ul>{sends?.map((row) => <li key={row.send_id}>{row.subject} — {row.outcome}</li>)}</ul>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
11
52
|
```
|
|
12
53
|
|
|
13
|
-
|
|
14
|
-
are not owned by this project and are squattable. `@pilely/core` is a peer
|
|
15
|
-
dependency; an app has exactly one facade, never two.
|
|
54
|
+
## Query keys
|
|
16
55
|
|
|
17
|
-
|
|
56
|
+
| Key | Query |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `["pilely", "simple-email", "sends", { limit }]` | `useSends` |
|
|
59
|
+
| `["pilely", "simple-email", "account"]` | `useEmailAccount` |
|
|
60
|
+
| `["pilely", "simple-email", "accounts", { limit }]` | `useEmailAccounts` |
|
|
61
|
+
| `["pilely", "simple-email", "templates", { limit }]` | `useTemplates` |
|
|
62
|
+
| `["pilely", "simple-email", "template", templateId]` | `useTemplate` |
|
|
18
63
|
|
|
19
|
-
|
|
20
|
-
`listSends()` put the app id in the request **body**; every account and
|
|
21
|
-
template method puts it in the request **path**. `listAccounts()` takes no
|
|
22
|
-
app id at all — it lists every account the caller owns. Every method adds
|
|
23
|
-
the id itself from `@pilely/core`'s `appId()`; it is never a parameter you
|
|
24
|
-
pass.
|
|
25
|
-
|
|
26
|
-
## Templates live under the account
|
|
64
|
+
## Templates can be nameless, and can lose their body
|
|
27
65
|
|
|
28
|
-
|
|
29
|
-
`accounts/{app}/templates/...` — a template belongs to one account, and
|
|
30
|
-
`createTemplate`/`listTemplates`/`templateInfo`/`updateTemplate`/`deleteTemplate`
|
|
31
|
-
all reach it that way.
|
|
66
|
+
`name` is `string | null`: a nameless template is legal. `html` on `useTemplate`'s data is optional, not nullable — when the stored body is missing the service omits the key. Check `"html" in template` before rendering it.
|
|
32
67
|
|
|
33
68
|
## Mock mode
|
|
34
69
|
|
|
35
|
-
Importing this package in a build made with `VITE_PILELY_MOCK=1` registers
|
|
36
|
-
an in-browser fake of `simple_email` behind `@pilely/core`'s mock runtime —
|
|
37
|
-
nothing else to call. Every method here then answers from memory with the
|
|
38
|
-
JSON the real service returns.
|
|
39
|
-
|
|
40
|
-
**Nothing is ever sent.** `send()` appends the message to an in-memory
|
|
41
|
-
outbox — the rows `listSends()` answers — and logs it to the console.
|
|
42
|
-
Accounts and templates are kept in memory and persisted to `localStorage`
|
|
43
|
-
across reloads; an app's account is created on its first send if it does
|
|
44
|
-
not exist yet. A missing account or template answers the bare 404, and a
|
|
45
|
-
second account for the same app answers `account_exists` (409). The
|
|
46
|
-
50-recipient bound on a send is kept, since it is the request's shape.
|
|
47
|
-
|
|
48
|
-
What it does not: send groups, the daily cap, the phone gate, rate limits
|
|
49
|
-
and credit checks.
|
|
70
|
+
Importing this package in a build made with `VITE_PILELY_MOCK=1` registers an in-browser fake of `simple_email` behind `@pilely/core`'s mock runtime — nothing else to call.
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
404, which reaches the app as `PilelyError(404, null)`, exactly where the
|
|
53
|
-
real service would hide it. Mock mode is on only in a Vite build with
|
|
54
|
-
`VITE_PILELY_MOCK=1`; see [`@pilely/core`](../core#mock-mode) for the
|
|
55
|
-
switch, `seedMock()` and `resetMock()`.
|
|
72
|
+
**Nothing is ever sent.** A send is appended to an in-memory outbox — the rows `useSends` answers — and logged to the console. Accounts and templates persist to `localStorage` across reloads; the app's account is created on its first send. A missing account or template answers the bare 404, and a second account for the same app answers `account_exists` (409). The 50-recipient bound is kept. It does not model send groups, the daily cap, the phone gate, rate limits or credit checks.
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
This is a pre-1.0 package and moves with the platform. Nothing here is
|
|
60
|
-
published to the registry yet. Zero runtime dependencies.
|
|
61
|
-
|
|
62
|
-
## Templates can be nameless, and can lose their body
|
|
63
|
-
|
|
64
|
-
Two degraded shapes the service keeps deliberately visible, both reflected
|
|
65
|
-
in the types:
|
|
66
|
-
|
|
67
|
-
- `name` is `string | null`. A nameless template is legal — `createTemplate`
|
|
68
|
-
takes `string | null` and hands the `null` straight back.
|
|
69
|
-
- `html` is **optional, not nullable**. When the stored object behind a
|
|
70
|
-
template is missing, `templateInfo` omits the key rather than sending
|
|
71
|
-
`null`. Discriminate with `"html" in template`, and never assume a body.
|
|
74
|
+
Writes need a signed-in user: a signed-out write answers the uniform bare 404, which reaches the hook's `error` as `PilelyError(404, null)`. See [`@pilely/core`](../core#mock-mode) for the switch, `seedMock()` and `resetMock()`.
|
|
72
75
|
|
|
73
76
|
## Error codes
|
|
74
77
|
|
|
75
|
-
Refusals arrive as `PilelyError` with a `code` drawn from
|
|
76
|
-
`SimpleEmailErrorCode`. Here matching on the code is not optional:
|
|
78
|
+
Refusals arrive as `PilelyError` in the hook's `error`, with a `code` drawn from `SimpleEmailErrorCode`. Match on the code: **`daily_cap_exceeded` and `rate_limited` both answer 429** — one means the app spent its UTC-day allowance (retry tomorrow), the other is a burst window (retry shortly).
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
app has spent its UTC-day send allowance (retry tomorrow, or not at all);
|
|
80
|
-
the other is an ordinary burst window (retry shortly). The status cannot
|
|
81
|
-
tell them apart — only the code can.
|
|
80
|
+
A send can also answer `phone_verification_required` (403), `phone_verification_unavailable` (503) and `out_of_traffic_credits` (503). Provisioning adds `account_exists`, `account_limit_reached` and `template_limit_reached` (all 409). The rest: `not_found` (404, the uniform hide), `bad_request` (400), `unauthenticated` (401), `bad_forwarded_identity` (401), `internal` (500). An oversize send or template body is refused before any handler runs, so its `code` is `null`.
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
`phone_verification_unavailable` (503) from the phone gate, and
|
|
85
|
-
`out_of_traffic_credits` (503). Provisioning adds `account_exists`,
|
|
86
|
-
`account_limit_reached` and `template_limit_reached` (all 409). The
|
|
87
|
-
remainder: `not_found` (404, the uniform hide), `bad_request` (400),
|
|
88
|
-
`unauthenticated` (401), `bad_forwarded_identity` (401), `internal` (500).
|
|
82
|
+
Limits: billed per recipient, at most 50 recipients per send, 200 sends per app per UTC day.
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
`out_of_traffic_credits` — worth knowing before debugging a member's send.
|
|
84
|
+
## Install
|
|
92
85
|
|
|
93
|
-
|
|
86
|
+
```sh
|
|
87
|
+
npm install @pilely/simple-email @pilely/core @tanstack/react-query
|
|
88
|
+
```
|
|
94
89
|
|
|
95
|
-
-
|
|
96
|
-
- At most 50 recipients per send.
|
|
97
|
-
- 200 sends per app per UTC day — the `daily_cap_exceeded` wall above.
|
|
98
|
-
- `send_id` is the idempotency key.
|
|
90
|
+
`@pilely/core`, `react` (`^18.3 || ^19`) and `@tanstack/react-query` (`^5`) are peer dependencies. Always install with the scope — unscoped names like `pilely-email` are not owned by this project. This is a pre-1.0 package and moves with the platform.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Account, AccountCursor, AccountInfoEnvelope, SendCursor, SendRow, Template, TemplateCursor, TemplateMeta } from "./types.js";
|
|
2
|
+
/** `to` is capped at the service's `max_recipients_per_send` (50 by default). */
|
|
3
|
+
export type SendContent = {
|
|
4
|
+
template_id: string;
|
|
5
|
+
variables?: Record<string, string>;
|
|
6
|
+
} | {
|
|
7
|
+
subject: string;
|
|
8
|
+
html: string;
|
|
9
|
+
};
|
|
10
|
+
export type SendInput = {
|
|
11
|
+
to: string[];
|
|
12
|
+
} & SendContent;
|
|
13
|
+
export interface ListSendsOptions {
|
|
14
|
+
limit?: number;
|
|
15
|
+
after_created_time_stamp?: number;
|
|
16
|
+
after_send_id?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ListAccountsOptions {
|
|
19
|
+
limit?: number;
|
|
20
|
+
after_created_time_stamp?: number;
|
|
21
|
+
after_app_id?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ListTemplatesOptions {
|
|
24
|
+
limit?: number;
|
|
25
|
+
after_created_time_stamp?: number;
|
|
26
|
+
after_template_id?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Both keys are REQUIRED — the server rejects either one missing with a
|
|
29
|
+
* 400 (`ContentField::validate`), matching `updateTemplate`'s full-replace
|
|
30
|
+
* contract: there is no patch form on this route. */
|
|
31
|
+
export interface UpdateTemplateBody {
|
|
32
|
+
subject: string;
|
|
33
|
+
html: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `app_id` is added from core's `appId()` — it is not a caller parameter.
|
|
37
|
+
* Body size is capped on this route (like `createTemplate`/`updateTemplate`):
|
|
38
|
+
* an oversize body is rejected by the server before any handler runs, so
|
|
39
|
+
* the failure is not an `{ok:false}` envelope and the thrown `PilelyError`
|
|
40
|
+
* carries a null `code`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function send(input: SendInput): Promise<{
|
|
43
|
+
send_id: string;
|
|
44
|
+
recipient_count: number;
|
|
45
|
+
}>;
|
|
46
|
+
/** `app_id` lives in the body here, like `send` — not in the path. The
|
|
47
|
+
* paged primitive; use `listAllSends` to walk to the end. */
|
|
48
|
+
export declare function listSends(options?: ListSendsOptions): Promise<{
|
|
49
|
+
sends: SendRow[];
|
|
50
|
+
next_cursor: SendCursor | null;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function listAllSends(): Promise<SendRow[]>;
|
|
53
|
+
/** `sendGroup` is optional here — omit it (or pass `undefined`) and the
|
|
54
|
+
* key is left off the wire, meaning owner-only. `setAccountAccess` below
|
|
55
|
+
* is the opposite: its key is always required. */
|
|
56
|
+
export declare function createAccount(sendGroup?: string | null): Promise<Account>;
|
|
57
|
+
/** No `app_id` anywhere on this route — `after_app_id` is a cursor field,
|
|
58
|
+
* not a selector. The paged primitive; use `listAllAccounts` to walk to
|
|
59
|
+
* the end. */
|
|
60
|
+
export declare function listAccounts(options?: ListAccountsOptions): Promise<{
|
|
61
|
+
accounts: Account[];
|
|
62
|
+
next_cursor: AccountCursor | null;
|
|
63
|
+
}>;
|
|
64
|
+
export declare function listAllAccounts(): Promise<Account[]>;
|
|
65
|
+
/** Returns the account plus its live operational state — the four extra
|
|
66
|
+
* fields are nullable siblings of `account`, not nested inside it. */
|
|
67
|
+
export declare function accountInfo(): Promise<AccountInfoEnvelope>;
|
|
68
|
+
export declare function deleteAccount(): Promise<void>;
|
|
69
|
+
/** `sendGroup` is REQUIRED here and its key is always sent, even `null` —
|
|
70
|
+
* the opposite of `createAccount`, where the key is optional. `null`
|
|
71
|
+
* means owner-only. */
|
|
72
|
+
export declare function setAccountAccess(sendGroup: string | null): Promise<Account>;
|
|
73
|
+
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
74
|
+
* back.
|
|
75
|
+
*
|
|
76
|
+
* `name` is nullable because the service accepts a nameless template. It
|
|
77
|
+
* stays a required POSITIONAL parameter — pass an explicit `null`, not
|
|
78
|
+
* nothing — and `TemplateMeta.name` then comes back `null`. */
|
|
79
|
+
export declare function createTemplate(name: string | null, subject: string, html: string): Promise<TemplateMeta>;
|
|
80
|
+
/** The paged primitive; use `listAllTemplates` to walk to the end. Rows
|
|
81
|
+
* carry metadata only — no `html`. */
|
|
82
|
+
export declare function listTemplates(options?: ListTemplatesOptions): Promise<{
|
|
83
|
+
templates: TemplateMeta[];
|
|
84
|
+
next_cursor: TemplateCursor | null;
|
|
85
|
+
}>;
|
|
86
|
+
export declare function listAllTemplates(): Promise<TemplateMeta[]>;
|
|
87
|
+
/** The only template route whose answer carries `html`. */
|
|
88
|
+
export declare function templateInfo(templateNanoid: string): Promise<Template>;
|
|
89
|
+
/**
|
|
90
|
+
* A FULL REPLACE of both `subject` and `html` — never a patch — and `name`
|
|
91
|
+
* is not updatable at all. Returns metadata only, like `createTemplate`.
|
|
92
|
+
* Subject to the same oversize-body caveat as `send`/`createTemplate`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function updateTemplate(templateNanoid: string, body: UpdateTemplateBody): Promise<TemplateMeta>;
|
|
95
|
+
export declare function deleteTemplate(templateNanoid: string): Promise<void>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// The simple_email service's 12 POST routes, one function each. This is the
|
|
2
|
+
// internal layer the hooks in hooks.ts call; the package exports the hooks,
|
|
3
|
+
// not these. app_id moves between the body and the path from route to
|
|
4
|
+
// route on this service.
|
|
5
|
+
import { appId, call, collectPages, isMockMode, registerMockService } from "@pilely/core";
|
|
6
|
+
import { createSimpleEmailFake } from "./mock.js";
|
|
7
|
+
// Importing this package (its hooks import this module) is all an app does
|
|
8
|
+
// to get its fake in mock mode. Dead code in a build without
|
|
9
|
+
// `VITE_PILELY_MOCK=1`, fake included.
|
|
10
|
+
if (isMockMode()) {
|
|
11
|
+
registerMockService("simple-email", createSimpleEmailFake);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every route on this service needs an app id on the wire (body or path).
|
|
15
|
+
* `@pilely/core`'s `appId()` can return `null` (no tag, `ready()` not yet
|
|
16
|
+
* resolved) — this service cannot send that: a `null` in a path template
|
|
17
|
+
* would silently become the literal string `"null"`. Fail loudly instead.
|
|
18
|
+
*/
|
|
19
|
+
function requireAppId() {
|
|
20
|
+
const id = appId();
|
|
21
|
+
if (!id) {
|
|
22
|
+
throw new Error('pilely client not loaded, <meta name="pilely-app"> missing, and no token pile_id claim available yet — call after ready()');
|
|
23
|
+
}
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
// ── Sending ──────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* `app_id` is added from core's `appId()` — it is not a caller parameter.
|
|
29
|
+
* Body size is capped on this route (like `createTemplate`/`updateTemplate`):
|
|
30
|
+
* an oversize body is rejected by the server before any handler runs, so
|
|
31
|
+
* the failure is not an `{ok:false}` envelope and the thrown `PilelyError`
|
|
32
|
+
* carries a null `code`.
|
|
33
|
+
*/
|
|
34
|
+
export async function send(input) {
|
|
35
|
+
const body = "template_id" in input
|
|
36
|
+
? { app_id: requireAppId(), to: input.to, template_id: input.template_id, variables: input.variables }
|
|
37
|
+
: { app_id: requireAppId(), to: input.to, subject: input.subject, html: input.html };
|
|
38
|
+
return call({
|
|
39
|
+
service: "simple-email",
|
|
40
|
+
path: "/send",
|
|
41
|
+
body,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** `app_id` lives in the body here, like `send` — not in the path. The
|
|
45
|
+
* paged primitive; use `listAllSends` to walk to the end. */
|
|
46
|
+
export async function listSends(options = {}) {
|
|
47
|
+
const json = await call({
|
|
48
|
+
service: "simple-email",
|
|
49
|
+
path: "/sends/list",
|
|
50
|
+
body: {
|
|
51
|
+
app_id: requireAppId(),
|
|
52
|
+
limit: options.limit,
|
|
53
|
+
after_created_time_stamp: options.after_created_time_stamp,
|
|
54
|
+
after_send_id: options.after_send_id,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return { sends: json.sends, next_cursor: json.next_cursor };
|
|
58
|
+
}
|
|
59
|
+
export async function listAllSends() {
|
|
60
|
+
return collectPages(async (cursor) => {
|
|
61
|
+
const page = await listSends({ limit: 100, ...(cursor ?? {}) });
|
|
62
|
+
return { rows: page.sends, nextCursor: page.next_cursor };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ── Accounts ─────────────────────────────────────────────────────────────
|
|
66
|
+
/** `sendGroup` is optional here — omit it (or pass `undefined`) and the
|
|
67
|
+
* key is left off the wire, meaning owner-only. `setAccountAccess` below
|
|
68
|
+
* is the opposite: its key is always required. */
|
|
69
|
+
export async function createAccount(sendGroup) {
|
|
70
|
+
const body = { app_id: requireAppId() };
|
|
71
|
+
if (sendGroup !== undefined) {
|
|
72
|
+
body.send_group = sendGroup;
|
|
73
|
+
}
|
|
74
|
+
const json = await call({
|
|
75
|
+
service: "simple-email",
|
|
76
|
+
path: "/accounts/create",
|
|
77
|
+
body,
|
|
78
|
+
});
|
|
79
|
+
return json.account;
|
|
80
|
+
}
|
|
81
|
+
/** No `app_id` anywhere on this route — `after_app_id` is a cursor field,
|
|
82
|
+
* not a selector. The paged primitive; use `listAllAccounts` to walk to
|
|
83
|
+
* the end. */
|
|
84
|
+
export async function listAccounts(options = {}) {
|
|
85
|
+
const json = await call({
|
|
86
|
+
service: "simple-email",
|
|
87
|
+
path: "/accounts/list",
|
|
88
|
+
body: {
|
|
89
|
+
limit: options.limit,
|
|
90
|
+
after_created_time_stamp: options.after_created_time_stamp,
|
|
91
|
+
after_app_id: options.after_app_id,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return { accounts: json.accounts, next_cursor: json.next_cursor };
|
|
95
|
+
}
|
|
96
|
+
export async function listAllAccounts() {
|
|
97
|
+
return collectPages(async (cursor) => {
|
|
98
|
+
const page = await listAccounts({ limit: 100, ...(cursor ?? {}) });
|
|
99
|
+
return { rows: page.accounts, nextCursor: page.next_cursor };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Returns the account plus its live operational state — the four extra
|
|
103
|
+
* fields are nullable siblings of `account`, not nested inside it. */
|
|
104
|
+
export async function accountInfo() {
|
|
105
|
+
const json = await call({
|
|
106
|
+
service: "simple-email",
|
|
107
|
+
path: `/accounts/${requireAppId()}/info`,
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
account: json.account,
|
|
111
|
+
daily_cap: json.daily_cap,
|
|
112
|
+
sent_today: json.sent_today,
|
|
113
|
+
credit_blocked: json.credit_blocked,
|
|
114
|
+
phone_verified: json.phone_verified,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export async function deleteAccount() {
|
|
118
|
+
await call({ service: "simple-email", path: `/accounts/${requireAppId()}/delete` });
|
|
119
|
+
}
|
|
120
|
+
/** `sendGroup` is REQUIRED here and its key is always sent, even `null` —
|
|
121
|
+
* the opposite of `createAccount`, where the key is optional. `null`
|
|
122
|
+
* means owner-only. */
|
|
123
|
+
export async function setAccountAccess(sendGroup) {
|
|
124
|
+
const json = await call({
|
|
125
|
+
service: "simple-email",
|
|
126
|
+
path: `/accounts/${requireAppId()}/access/set`,
|
|
127
|
+
body: { send_group: sendGroup },
|
|
128
|
+
});
|
|
129
|
+
return json.account;
|
|
130
|
+
}
|
|
131
|
+
// ── Templates (nested under the account) ────────────────────────────────
|
|
132
|
+
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
133
|
+
* back.
|
|
134
|
+
*
|
|
135
|
+
* `name` is nullable because the service accepts a nameless template. It
|
|
136
|
+
* stays a required POSITIONAL parameter — pass an explicit `null`, not
|
|
137
|
+
* nothing — and `TemplateMeta.name` then comes back `null`. */
|
|
138
|
+
export async function createTemplate(name, subject, html) {
|
|
139
|
+
const json = await call({
|
|
140
|
+
service: "simple-email",
|
|
141
|
+
path: `/accounts/${requireAppId()}/templates/create`,
|
|
142
|
+
body: { name, subject, html },
|
|
143
|
+
});
|
|
144
|
+
return json.template;
|
|
145
|
+
}
|
|
146
|
+
/** The paged primitive; use `listAllTemplates` to walk to the end. Rows
|
|
147
|
+
* carry metadata only — no `html`. */
|
|
148
|
+
export async function listTemplates(options = {}) {
|
|
149
|
+
const json = await call({
|
|
150
|
+
service: "simple-email",
|
|
151
|
+
path: `/accounts/${requireAppId()}/templates/list`,
|
|
152
|
+
body: {
|
|
153
|
+
limit: options.limit,
|
|
154
|
+
after_created_time_stamp: options.after_created_time_stamp,
|
|
155
|
+
after_template_id: options.after_template_id,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return { templates: json.templates, next_cursor: json.next_cursor };
|
|
159
|
+
}
|
|
160
|
+
export async function listAllTemplates() {
|
|
161
|
+
return collectPages(async (cursor) => {
|
|
162
|
+
const page = await listTemplates({ limit: 100, ...(cursor ?? {}) });
|
|
163
|
+
return { rows: page.templates, nextCursor: page.next_cursor };
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** The only template route whose answer carries `html`. */
|
|
167
|
+
export async function templateInfo(templateNanoid) {
|
|
168
|
+
const json = await call({
|
|
169
|
+
service: "simple-email",
|
|
170
|
+
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/info`,
|
|
171
|
+
});
|
|
172
|
+
return json.template;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A FULL REPLACE of both `subject` and `html` — never a patch — and `name`
|
|
176
|
+
* is not updatable at all. Returns metadata only, like `createTemplate`.
|
|
177
|
+
* Subject to the same oversize-body caveat as `send`/`createTemplate`.
|
|
178
|
+
*/
|
|
179
|
+
export async function updateTemplate(templateNanoid, body) {
|
|
180
|
+
const json = await call({
|
|
181
|
+
service: "simple-email",
|
|
182
|
+
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/update`,
|
|
183
|
+
body,
|
|
184
|
+
});
|
|
185
|
+
return json.template;
|
|
186
|
+
}
|
|
187
|
+
export async function deleteTemplate(templateNanoid) {
|
|
188
|
+
await call({
|
|
189
|
+
service: "simple-email",
|
|
190
|
+
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/delete`,
|
|
191
|
+
});
|
|
192
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { UseInfiniteQueryResult, UseMutationResult, UseQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import type { SendInput } from "./api.js";
|
|
3
|
+
import type { Account, AccountInfoEnvelope, SendRow, Template, TemplateMeta } from "./types.js";
|
|
4
|
+
export interface PageOptions {
|
|
5
|
+
/** Rows per page; the server defaults to 50 and caps at 100. */
|
|
6
|
+
limit?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CreateTemplateInput {
|
|
9
|
+
/** `null` for a nameless template. */
|
|
10
|
+
name: string | null;
|
|
11
|
+
subject: string;
|
|
12
|
+
html: string;
|
|
13
|
+
}
|
|
14
|
+
/** A full replace of both `subject` and `html`; `name` is not updatable. */
|
|
15
|
+
export interface UpdateTemplateInput {
|
|
16
|
+
templateId: string;
|
|
17
|
+
subject: string;
|
|
18
|
+
html: string;
|
|
19
|
+
}
|
|
20
|
+
/** Sends one email from this app's account. Not optimistic: a send counts
|
|
21
|
+
* only once the server accepted it. */
|
|
22
|
+
export declare function useSend(): UseMutationResult<{
|
|
23
|
+
send_id: string;
|
|
24
|
+
recipient_count: number;
|
|
25
|
+
}, Error, SendInput>;
|
|
26
|
+
/** This app's outbox, newest first, as an infinite query. */
|
|
27
|
+
export declare function useSends(options?: PageOptions): UseInfiniteQueryResult<SendRow[], Error>;
|
|
28
|
+
/** The caller's email accounts across apps, as an infinite query. */
|
|
29
|
+
export declare function useEmailAccounts(options?: PageOptions): UseInfiniteQueryResult<Account[], Error>;
|
|
30
|
+
/** This app's account with its live state: daily cap, sent today, credit
|
|
31
|
+
* and phone verification. */
|
|
32
|
+
export declare function useEmailAccount(): UseQueryResult<AccountInfoEnvelope, Error>;
|
|
33
|
+
/** Creates this app's account; the variable is its optional send group
|
|
34
|
+
* (`null` or omitted: owner-only). */
|
|
35
|
+
export declare function useCreateEmailAccount(): UseMutationResult<Account, Error, string | null | void>;
|
|
36
|
+
/** Deletes this app's account and the templates under it. */
|
|
37
|
+
export declare function useDeleteEmailAccount(): UseMutationResult<void, Error, void>;
|
|
38
|
+
/** Replaces this app's send group; `null` means owner-only. */
|
|
39
|
+
export declare function useSetEmailAccountAccess(): UseMutationResult<Account, Error, string | null>;
|
|
40
|
+
/** This app's templates (metadata only), as an infinite query. */
|
|
41
|
+
export declare function useTemplates(options?: PageOptions): UseInfiniteQueryResult<TemplateMeta[], Error>;
|
|
42
|
+
/** One template with its `html`. An `undefined` id disables the query. */
|
|
43
|
+
export declare function useTemplate(templateId: string | undefined): UseQueryResult<Template, Error>;
|
|
44
|
+
export declare function useCreateTemplate(): UseMutationResult<TemplateMeta, Error, CreateTemplateInput>;
|
|
45
|
+
export declare function useUpdateTemplate(): UseMutationResult<TemplateMeta, Error, UpdateTemplateInput>;
|
|
46
|
+
export declare function useDeleteTemplate(): UseMutationResult<void, Error, string>;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { serviceQueryKey, useServiceInfiniteQuery, useServiceMutation, useServiceQuery } from "@pilely/core";
|
|
2
|
+
import { accountInfo, createAccount, createTemplate, deleteAccount, deleteTemplate, listAccounts, listSends, listTemplates, send, setAccountAccess, templateInfo, updateTemplate, } from "./api.js";
|
|
3
|
+
/** `["pilely", "simple-email", "sends", options]` */
|
|
4
|
+
const sendsKey = () => serviceQueryKey("simple-email", "sends");
|
|
5
|
+
/** `["pilely", "simple-email", "accounts", options]` */
|
|
6
|
+
const accountsKey = () => serviceQueryKey("simple-email", "accounts");
|
|
7
|
+
/** `["pilely", "simple-email", "account"]` — this app's account info. */
|
|
8
|
+
const accountKey = () => serviceQueryKey("simple-email", "account");
|
|
9
|
+
/** `["pilely", "simple-email", "templates", options]` */
|
|
10
|
+
const templatesKey = () => serviceQueryKey("simple-email", "templates");
|
|
11
|
+
/** `["pilely", "simple-email", "template", templateId]` */
|
|
12
|
+
const templateKey = (templateId) => serviceQueryKey("simple-email", "template", templateId);
|
|
13
|
+
// ── Sending ──────────────────────────────────────────────────────────────
|
|
14
|
+
/** Sends one email from this app's account. Not optimistic: a send counts
|
|
15
|
+
* only once the server accepted it. */
|
|
16
|
+
export function useSend() {
|
|
17
|
+
return useServiceMutation("useSend", {
|
|
18
|
+
mutationFn: send,
|
|
19
|
+
invalidates: () => [sendsKey()],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** This app's outbox, newest first, as an infinite query. */
|
|
23
|
+
export function useSends(options = {}) {
|
|
24
|
+
const { limit } = options;
|
|
25
|
+
return useServiceInfiniteQuery("useSends", {
|
|
26
|
+
queryKey: [...sendsKey(), { limit }],
|
|
27
|
+
fetchPage: async (cursor) => {
|
|
28
|
+
const page = await listSends({ limit, ...(cursor ?? {}) });
|
|
29
|
+
return { rows: page.sends, nextCursor: page.next_cursor };
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// ── Accounts ─────────────────────────────────────────────────────────────
|
|
34
|
+
/** The caller's email accounts across apps, as an infinite query. */
|
|
35
|
+
export function useEmailAccounts(options = {}) {
|
|
36
|
+
const { limit } = options;
|
|
37
|
+
return useServiceInfiniteQuery("useEmailAccounts", {
|
|
38
|
+
queryKey: [...accountsKey(), { limit }],
|
|
39
|
+
fetchPage: async (cursor) => {
|
|
40
|
+
const page = await listAccounts({ limit, ...(cursor ?? {}) });
|
|
41
|
+
return { rows: page.accounts, nextCursor: page.next_cursor };
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** This app's account with its live state: daily cap, sent today, credit
|
|
46
|
+
* and phone verification. */
|
|
47
|
+
export function useEmailAccount() {
|
|
48
|
+
return useServiceQuery("useEmailAccount", { queryKey: accountKey(), queryFn: accountInfo });
|
|
49
|
+
}
|
|
50
|
+
/** Creates this app's account; the variable is its optional send group
|
|
51
|
+
* (`null` or omitted: owner-only). */
|
|
52
|
+
export function useCreateEmailAccount() {
|
|
53
|
+
return useServiceMutation("useCreateEmailAccount", {
|
|
54
|
+
mutationFn: (sendGroup) => createAccount(sendGroup ?? undefined),
|
|
55
|
+
invalidates: () => [accountsKey(), accountKey()],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Deletes this app's account and the templates under it. */
|
|
59
|
+
export function useDeleteEmailAccount() {
|
|
60
|
+
return useServiceMutation("useDeleteEmailAccount", {
|
|
61
|
+
mutationFn: () => deleteAccount(),
|
|
62
|
+
invalidates: () => [accountsKey(), accountKey(), templatesKey(), serviceQueryKey("simple-email", "template")],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Replaces this app's send group; `null` means owner-only. */
|
|
66
|
+
export function useSetEmailAccountAccess() {
|
|
67
|
+
return useServiceMutation("useSetEmailAccountAccess", {
|
|
68
|
+
mutationFn: setAccountAccess,
|
|
69
|
+
invalidates: () => [accountsKey(), accountKey()],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// ── Templates ────────────────────────────────────────────────────────────
|
|
73
|
+
/** This app's templates (metadata only), as an infinite query. */
|
|
74
|
+
export function useTemplates(options = {}) {
|
|
75
|
+
const { limit } = options;
|
|
76
|
+
return useServiceInfiniteQuery("useTemplates", {
|
|
77
|
+
queryKey: [...templatesKey(), { limit }],
|
|
78
|
+
fetchPage: async (cursor) => {
|
|
79
|
+
const page = await listTemplates({ limit, ...(cursor ?? {}) });
|
|
80
|
+
return { rows: page.templates, nextCursor: page.next_cursor };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** One template with its `html`. An `undefined` id disables the query. */
|
|
85
|
+
export function useTemplate(templateId) {
|
|
86
|
+
return useServiceQuery("useTemplate", {
|
|
87
|
+
queryKey: templateKey(templateId),
|
|
88
|
+
queryFn: () => templateInfo(templateId),
|
|
89
|
+
enabled: templateId !== undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function useCreateTemplate() {
|
|
93
|
+
return useServiceMutation("useCreateTemplate", {
|
|
94
|
+
mutationFn: ({ name, subject, html }) => createTemplate(name, subject, html),
|
|
95
|
+
invalidates: () => [templatesKey()],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export function useUpdateTemplate() {
|
|
99
|
+
return useServiceMutation("useUpdateTemplate", {
|
|
100
|
+
mutationFn: ({ templateId, subject, html }) => updateTemplate(templateId, { subject, html }),
|
|
101
|
+
invalidates: (_template, { templateId }) => [templatesKey(), templateKey(templateId)],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function useDeleteTemplate() {
|
|
105
|
+
return useServiceMutation("useDeleteTemplate", {
|
|
106
|
+
mutationFn: deleteTemplate,
|
|
107
|
+
invalidates: (_none, templateId) => [templatesKey(), templateKey(templateId)],
|
|
108
|
+
});
|
|
109
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,96 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export { useCreateEmailAccount, useCreateTemplate, useDeleteEmailAccount, useDeleteTemplate, useEmailAccount, useEmailAccounts, useSend, useSends, useSetEmailAccountAccess, useTemplate, useTemplates, useUpdateTemplate, } from "./hooks.js";
|
|
2
|
+
export type { CreateTemplateInput, PageOptions, UpdateTemplateInput } from "./hooks.js";
|
|
3
|
+
export type { SendContent, SendInput } from "./api.js";
|
|
2
4
|
export type { Account, SimpleEmailErrorCode, AccountCursor, AccountInfoEnvelope, SendCursor, SendRow, Template, TemplateCursor, TemplateMeta, } from "./types.js";
|
|
3
|
-
/** `to` is capped at the service's `max_recipients_per_send` (50 by default). */
|
|
4
|
-
export type SendContent = {
|
|
5
|
-
template_id: string;
|
|
6
|
-
variables?: Record<string, string>;
|
|
7
|
-
} | {
|
|
8
|
-
subject: string;
|
|
9
|
-
html: string;
|
|
10
|
-
};
|
|
11
|
-
export type SendInput = {
|
|
12
|
-
to: string[];
|
|
13
|
-
} & SendContent;
|
|
14
|
-
export interface ListSendsOptions {
|
|
15
|
-
limit?: number;
|
|
16
|
-
after_created_time_stamp?: number;
|
|
17
|
-
after_send_id?: string;
|
|
18
|
-
}
|
|
19
|
-
export interface ListAccountsOptions {
|
|
20
|
-
limit?: number;
|
|
21
|
-
after_created_time_stamp?: number;
|
|
22
|
-
after_app_id?: string;
|
|
23
|
-
}
|
|
24
|
-
export interface ListTemplatesOptions {
|
|
25
|
-
limit?: number;
|
|
26
|
-
after_created_time_stamp?: number;
|
|
27
|
-
after_template_id?: string;
|
|
28
|
-
}
|
|
29
|
-
/** Both keys are REQUIRED — the server rejects either one missing with a
|
|
30
|
-
* 400 (`ContentField::validate`), matching `updateTemplate`'s full-replace
|
|
31
|
-
* contract: there is no patch form on this route. */
|
|
32
|
-
export interface UpdateTemplateBody {
|
|
33
|
-
subject: string;
|
|
34
|
-
html: string;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* `app_id` is added from core's `appId()` — it is not a caller parameter.
|
|
38
|
-
* Body size is capped on this route (like `createTemplate`/`updateTemplate`):
|
|
39
|
-
* an oversize body is rejected by the server before any handler runs, so
|
|
40
|
-
* the failure is not an `{ok:false}` envelope and the thrown `PilelyError`
|
|
41
|
-
* carries a null `code`.
|
|
42
|
-
*/
|
|
43
|
-
export declare function send(input: SendInput): Promise<{
|
|
44
|
-
send_id: string;
|
|
45
|
-
recipient_count: number;
|
|
46
|
-
}>;
|
|
47
|
-
/** `app_id` lives in the body here, like `send` — not in the path. The
|
|
48
|
-
* paged primitive; use `listAllSends` to walk to the end. */
|
|
49
|
-
export declare function listSends(options?: ListSendsOptions): Promise<{
|
|
50
|
-
sends: SendRow[];
|
|
51
|
-
next_cursor: SendCursor | null;
|
|
52
|
-
}>;
|
|
53
|
-
export declare function listAllSends(): Promise<SendRow[]>;
|
|
54
|
-
/** `sendGroup` is optional here — omit it (or pass `undefined`) and the
|
|
55
|
-
* key is left off the wire, meaning owner-only. `setAccountAccess` below
|
|
56
|
-
* is the opposite: its key is always required. */
|
|
57
|
-
export declare function createAccount(sendGroup?: string | null): Promise<Account>;
|
|
58
|
-
/** No `app_id` anywhere on this route — `after_app_id` is a cursor field,
|
|
59
|
-
* not a selector. The paged primitive; use `listAllAccounts` to walk to
|
|
60
|
-
* the end. */
|
|
61
|
-
export declare function listAccounts(options?: ListAccountsOptions): Promise<{
|
|
62
|
-
accounts: Account[];
|
|
63
|
-
next_cursor: AccountCursor | null;
|
|
64
|
-
}>;
|
|
65
|
-
export declare function listAllAccounts(): Promise<Account[]>;
|
|
66
|
-
/** Returns the account plus its live operational state — the four extra
|
|
67
|
-
* fields are nullable siblings of `account`, not nested inside it. */
|
|
68
|
-
export declare function accountInfo(): Promise<AccountInfoEnvelope>;
|
|
69
|
-
export declare function deleteAccount(): Promise<void>;
|
|
70
|
-
/** `sendGroup` is REQUIRED here and its key is always sent, even `null` —
|
|
71
|
-
* the opposite of `createAccount`, where the key is optional. `null`
|
|
72
|
-
* means owner-only. */
|
|
73
|
-
export declare function setAccountAccess(sendGroup: string | null): Promise<Account>;
|
|
74
|
-
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
75
|
-
* back.
|
|
76
|
-
*
|
|
77
|
-
* `name` is nullable because the service accepts a nameless template. It
|
|
78
|
-
* stays a required POSITIONAL parameter — pass an explicit `null`, not
|
|
79
|
-
* nothing — and `TemplateMeta.name` then comes back `null`. */
|
|
80
|
-
export declare function createTemplate(name: string | null, subject: string, html: string): Promise<TemplateMeta>;
|
|
81
|
-
/** The paged primitive; use `listAllTemplates` to walk to the end. Rows
|
|
82
|
-
* carry metadata only — no `html`. */
|
|
83
|
-
export declare function listTemplates(options?: ListTemplatesOptions): Promise<{
|
|
84
|
-
templates: TemplateMeta[];
|
|
85
|
-
next_cursor: TemplateCursor | null;
|
|
86
|
-
}>;
|
|
87
|
-
export declare function listAllTemplates(): Promise<TemplateMeta[]>;
|
|
88
|
-
/** The only template route whose answer carries `html`. */
|
|
89
|
-
export declare function templateInfo(templateNanoid: string): Promise<Template>;
|
|
90
|
-
/**
|
|
91
|
-
* A FULL REPLACE of both `subject` and `html` — never a patch — and `name`
|
|
92
|
-
* is not updatable at all. Returns metadata only, like `createTemplate`.
|
|
93
|
-
* Subject to the same oversize-body caveat as `send`/`createTemplate`.
|
|
94
|
-
*/
|
|
95
|
-
export declare function updateTemplate(templateNanoid: string, body: UpdateTemplateBody): Promise<TemplateMeta>;
|
|
96
|
-
export declare function deleteTemplate(templateNanoid: string): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,190 +1,4 @@
|
|
|
1
|
-
// @pilely/simple-email —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { createSimpleEmailFake } from "./mock.js";
|
|
6
|
-
// Importing this package is all an app does to get its fake in mock mode.
|
|
7
|
-
// Dead code in a build without `VITE_PILELY_MOCK=1`, fake included.
|
|
8
|
-
if (isMockMode()) {
|
|
9
|
-
registerMockService("simple-email", createSimpleEmailFake);
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Every route on this service needs an app id on the wire (body or path).
|
|
13
|
-
* `@pilely/core`'s `appId()` can return `null` (no tag, `ready()` not yet
|
|
14
|
-
* resolved) — this service cannot send that: a `null` in a path template
|
|
15
|
-
* would silently become the literal string `"null"`. Fail loudly instead.
|
|
16
|
-
*/
|
|
17
|
-
function requireAppId() {
|
|
18
|
-
const id = appId();
|
|
19
|
-
if (!id) {
|
|
20
|
-
throw new Error('pilely client not loaded, <meta name="pilely-app"> missing, and no token pile_id claim available yet — call after ready()');
|
|
21
|
-
}
|
|
22
|
-
return id;
|
|
23
|
-
}
|
|
24
|
-
// ── Sending ──────────────────────────────────────────────────────────────
|
|
25
|
-
/**
|
|
26
|
-
* `app_id` is added from core's `appId()` — it is not a caller parameter.
|
|
27
|
-
* Body size is capped on this route (like `createTemplate`/`updateTemplate`):
|
|
28
|
-
* an oversize body is rejected by the server before any handler runs, so
|
|
29
|
-
* the failure is not an `{ok:false}` envelope and the thrown `PilelyError`
|
|
30
|
-
* carries a null `code`.
|
|
31
|
-
*/
|
|
32
|
-
export async function send(input) {
|
|
33
|
-
const body = "template_id" in input
|
|
34
|
-
? { app_id: requireAppId(), to: input.to, template_id: input.template_id, variables: input.variables }
|
|
35
|
-
: { app_id: requireAppId(), to: input.to, subject: input.subject, html: input.html };
|
|
36
|
-
return call({
|
|
37
|
-
service: "simple-email",
|
|
38
|
-
path: "/send",
|
|
39
|
-
body,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/** `app_id` lives in the body here, like `send` — not in the path. The
|
|
43
|
-
* paged primitive; use `listAllSends` to walk to the end. */
|
|
44
|
-
export async function listSends(options = {}) {
|
|
45
|
-
const json = await call({
|
|
46
|
-
service: "simple-email",
|
|
47
|
-
path: "/sends/list",
|
|
48
|
-
body: {
|
|
49
|
-
app_id: requireAppId(),
|
|
50
|
-
limit: options.limit,
|
|
51
|
-
after_created_time_stamp: options.after_created_time_stamp,
|
|
52
|
-
after_send_id: options.after_send_id,
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
return { sends: json.sends, next_cursor: json.next_cursor };
|
|
56
|
-
}
|
|
57
|
-
export async function listAllSends() {
|
|
58
|
-
return collectPages(async (cursor) => {
|
|
59
|
-
const page = await listSends({ limit: 100, ...(cursor ?? {}) });
|
|
60
|
-
return { rows: page.sends, nextCursor: page.next_cursor };
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
// ── Accounts ─────────────────────────────────────────────────────────────
|
|
64
|
-
/** `sendGroup` is optional here — omit it (or pass `undefined`) and the
|
|
65
|
-
* key is left off the wire, meaning owner-only. `setAccountAccess` below
|
|
66
|
-
* is the opposite: its key is always required. */
|
|
67
|
-
export async function createAccount(sendGroup) {
|
|
68
|
-
const body = { app_id: requireAppId() };
|
|
69
|
-
if (sendGroup !== undefined) {
|
|
70
|
-
body.send_group = sendGroup;
|
|
71
|
-
}
|
|
72
|
-
const json = await call({
|
|
73
|
-
service: "simple-email",
|
|
74
|
-
path: "/accounts/create",
|
|
75
|
-
body,
|
|
76
|
-
});
|
|
77
|
-
return json.account;
|
|
78
|
-
}
|
|
79
|
-
/** No `app_id` anywhere on this route — `after_app_id` is a cursor field,
|
|
80
|
-
* not a selector. The paged primitive; use `listAllAccounts` to walk to
|
|
81
|
-
* the end. */
|
|
82
|
-
export async function listAccounts(options = {}) {
|
|
83
|
-
const json = await call({
|
|
84
|
-
service: "simple-email",
|
|
85
|
-
path: "/accounts/list",
|
|
86
|
-
body: {
|
|
87
|
-
limit: options.limit,
|
|
88
|
-
after_created_time_stamp: options.after_created_time_stamp,
|
|
89
|
-
after_app_id: options.after_app_id,
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
return { accounts: json.accounts, next_cursor: json.next_cursor };
|
|
93
|
-
}
|
|
94
|
-
export async function listAllAccounts() {
|
|
95
|
-
return collectPages(async (cursor) => {
|
|
96
|
-
const page = await listAccounts({ limit: 100, ...(cursor ?? {}) });
|
|
97
|
-
return { rows: page.accounts, nextCursor: page.next_cursor };
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
/** Returns the account plus its live operational state — the four extra
|
|
101
|
-
* fields are nullable siblings of `account`, not nested inside it. */
|
|
102
|
-
export async function accountInfo() {
|
|
103
|
-
const json = await call({
|
|
104
|
-
service: "simple-email",
|
|
105
|
-
path: `/accounts/${requireAppId()}/info`,
|
|
106
|
-
});
|
|
107
|
-
return {
|
|
108
|
-
account: json.account,
|
|
109
|
-
daily_cap: json.daily_cap,
|
|
110
|
-
sent_today: json.sent_today,
|
|
111
|
-
credit_blocked: json.credit_blocked,
|
|
112
|
-
phone_verified: json.phone_verified,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
export async function deleteAccount() {
|
|
116
|
-
await call({ service: "simple-email", path: `/accounts/${requireAppId()}/delete` });
|
|
117
|
-
}
|
|
118
|
-
/** `sendGroup` is REQUIRED here and its key is always sent, even `null` —
|
|
119
|
-
* the opposite of `createAccount`, where the key is optional. `null`
|
|
120
|
-
* means owner-only. */
|
|
121
|
-
export async function setAccountAccess(sendGroup) {
|
|
122
|
-
const json = await call({
|
|
123
|
-
service: "simple-email",
|
|
124
|
-
path: `/accounts/${requireAppId()}/access/set`,
|
|
125
|
-
body: { send_group: sendGroup },
|
|
126
|
-
});
|
|
127
|
-
return json.account;
|
|
128
|
-
}
|
|
129
|
-
// ── Templates (nested under the account) ────────────────────────────────
|
|
130
|
-
/** Returns metadata only — no `html`. Use `templateInfo` to read the body
|
|
131
|
-
* back.
|
|
132
|
-
*
|
|
133
|
-
* `name` is nullable because the service accepts a nameless template. It
|
|
134
|
-
* stays a required POSITIONAL parameter — pass an explicit `null`, not
|
|
135
|
-
* nothing — and `TemplateMeta.name` then comes back `null`. */
|
|
136
|
-
export async function createTemplate(name, subject, html) {
|
|
137
|
-
const json = await call({
|
|
138
|
-
service: "simple-email",
|
|
139
|
-
path: `/accounts/${requireAppId()}/templates/create`,
|
|
140
|
-
body: { name, subject, html },
|
|
141
|
-
});
|
|
142
|
-
return json.template;
|
|
143
|
-
}
|
|
144
|
-
/** The paged primitive; use `listAllTemplates` to walk to the end. Rows
|
|
145
|
-
* carry metadata only — no `html`. */
|
|
146
|
-
export async function listTemplates(options = {}) {
|
|
147
|
-
const json = await call({
|
|
148
|
-
service: "simple-email",
|
|
149
|
-
path: `/accounts/${requireAppId()}/templates/list`,
|
|
150
|
-
body: {
|
|
151
|
-
limit: options.limit,
|
|
152
|
-
after_created_time_stamp: options.after_created_time_stamp,
|
|
153
|
-
after_template_id: options.after_template_id,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
return { templates: json.templates, next_cursor: json.next_cursor };
|
|
157
|
-
}
|
|
158
|
-
export async function listAllTemplates() {
|
|
159
|
-
return collectPages(async (cursor) => {
|
|
160
|
-
const page = await listTemplates({ limit: 100, ...(cursor ?? {}) });
|
|
161
|
-
return { rows: page.templates, nextCursor: page.next_cursor };
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
/** The only template route whose answer carries `html`. */
|
|
165
|
-
export async function templateInfo(templateNanoid) {
|
|
166
|
-
const json = await call({
|
|
167
|
-
service: "simple-email",
|
|
168
|
-
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/info`,
|
|
169
|
-
});
|
|
170
|
-
return json.template;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* A FULL REPLACE of both `subject` and `html` — never a patch — and `name`
|
|
174
|
-
* is not updatable at all. Returns metadata only, like `createTemplate`.
|
|
175
|
-
* Subject to the same oversize-body caveat as `send`/`createTemplate`.
|
|
176
|
-
*/
|
|
177
|
-
export async function updateTemplate(templateNanoid, body) {
|
|
178
|
-
const json = await call({
|
|
179
|
-
service: "simple-email",
|
|
180
|
-
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/update`,
|
|
181
|
-
body,
|
|
182
|
-
});
|
|
183
|
-
return json.template;
|
|
184
|
-
}
|
|
185
|
-
export async function deleteTemplate(templateNanoid) {
|
|
186
|
-
await call({
|
|
187
|
-
service: "simple-email",
|
|
188
|
-
path: `/accounts/${requireAppId()}/templates/${templateNanoid}/delete`,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
1
|
+
// @pilely/simple-email — React hooks over the simple_email service: a query
|
|
2
|
+
// hook for every read, a mutation hook for every write. Render them inside
|
|
3
|
+
// @pilely/core's <PilelyProvider>.
|
|
4
|
+
export { useCreateEmailAccount, useCreateTemplate, useDeleteEmailAccount, useDeleteTemplate, useEmailAccount, useEmailAccounts, useSend, useSends, useSetEmailAccountAccess, useTemplate, useTemplates, useUpdateTemplate, } from "./hooks.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilely/simple-email",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "React hooks for the simple_email service.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"directory": "npm/simple-email"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@pilely/core": "^0.
|
|
24
|
+
"@pilely/core": "^0.4.0",
|
|
25
|
+
"@tanstack/react-query": "^5.0.0",
|
|
26
|
+
"react": "^18.3.0 || ^19.0.0"
|
|
25
27
|
},
|
|
26
28
|
"scripts": {
|
|
27
29
|
"build": "tsc -p tsconfig.json",
|