@marlinjai/mail-sdk 0.6.0 → 0.7.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 +91 -0
- package/dist/index.d.mts +474 -91
- package/dist/index.d.ts +474 -91
- package/dist/index.js +10 -0
- package/dist/index.mjs +10 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -197,6 +197,97 @@ Testing against a fake: implement the same replay rule (same key and body,
|
|
|
197
197
|
same answer), `addRecipients` idempotent on the address, and at most one
|
|
198
198
|
recipient on a letter, or the tests prove less than the real service does.
|
|
199
199
|
|
|
200
|
+
## Declaring what a template receives
|
|
201
|
+
|
|
202
|
+
A template's editor cannot see your code, so on its own it cannot say that
|
|
203
|
+
`reason` is your contact form's "Reason" drop-down, which values it sends, or
|
|
204
|
+
that `first_name` may be blank. Your app can say so: **declare each template's
|
|
205
|
+
inputs** from the same constants your forms use, on every deploy. The editor
|
|
206
|
+
then shows where every value comes from, names an insertion's rows by your
|
|
207
|
+
labels ("I manage a venue", not `venue`), and starts its example values from
|
|
208
|
+
your examples (`docs/public/mail-contract.md`, "Template inputs").
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { createMailClient, templateInputProblems, type TemplateInputsDeclaration } from '@marlinjai/mail-sdk';
|
|
212
|
+
|
|
213
|
+
const mail = createMailClient({ baseUrl: process.env.MAIL_SERVICE_URL!, apiKey: process.env.MAIL_SERVICE_API_KEY! });
|
|
214
|
+
|
|
215
|
+
// Built from the form's own constants, never typed twice.
|
|
216
|
+
const contactReasons = ['general', 'venue', 'press'] as const;
|
|
217
|
+
const reasonLabels: Record<(typeof contactReasons)[number], string> = {
|
|
218
|
+
general: 'General question',
|
|
219
|
+
venue: 'I manage a venue',
|
|
220
|
+
press: 'Press & Media',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const contactInputs: TemplateInputsDeclaration['inputs'] = {
|
|
224
|
+
first_name: {
|
|
225
|
+
kind: 'text',
|
|
226
|
+
label: 'First name',
|
|
227
|
+
source: { kind: 'form_field', form: 'Contact form', field: 'Name' },
|
|
228
|
+
optional: true,
|
|
229
|
+
example: 'Anna',
|
|
230
|
+
},
|
|
231
|
+
reason: {
|
|
232
|
+
kind: 'one_of',
|
|
233
|
+
label: 'Contact reason',
|
|
234
|
+
source: {
|
|
235
|
+
kind: 'form_field',
|
|
236
|
+
form: 'Contact form',
|
|
237
|
+
field: 'Reason (dropdown)',
|
|
238
|
+
note: 'Also set by /contact?reason=; anything unknown is sent as general.',
|
|
239
|
+
},
|
|
240
|
+
values: contactReasons.map((value) => ({ value, label: reasonLabels[value] })),
|
|
241
|
+
example: 'venue',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** Run after each deploy (or at server start), once per template the app sends from. */
|
|
246
|
+
export async function declareInputs(commit: string) {
|
|
247
|
+
const result = await mail.templates.declareInputs(
|
|
248
|
+
process.env.MAIL_TEMPLATE_CONTACT_CONFIRMATION!,
|
|
249
|
+
{ app: 'ŌPUNTIA website', app_version: commit, inputs: contactInputs },
|
|
250
|
+
{ idempotencyKey: `inputs:contact:${commit}` },
|
|
251
|
+
);
|
|
252
|
+
return result.changed; // false when this commit declared exactly this already
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
What each piece does, and why:
|
|
257
|
+
|
|
258
|
+
- **The kinds** decide how the editor names an insertion's rows: `boolean`
|
|
259
|
+
(Yes and No; `false` and absent are both No), `text` and `url` (Has a value
|
|
260
|
+
and Is empty), `one_of` (a row per value with your label, then Anything else
|
|
261
|
+
and Not given). `values[].value` is exactly what your form sends, in an
|
|
262
|
+
insertion choice key's syntax (lowercase letters, digits, `-` and `_`).
|
|
263
|
+
- **Where it comes from:** `form_field` names the form and the field as a
|
|
264
|
+
person sees them on your site; `derived` says in a `note` how your code works
|
|
265
|
+
it out ("the joined gathering's title in the visitor's language", "whether
|
|
266
|
+
the optional field was answered; the text is never sent").
|
|
267
|
+
- **Only what you send.** `email` and `unsubscribe_url` are filled by the
|
|
268
|
+
service and refused (`input_reserved`); `first_name` and `last_name` are
|
|
269
|
+
yours to declare, since your merge values come first.
|
|
270
|
+
- **Keep it true in your tests.** `templateInputProblems(inputs)` runs the
|
|
271
|
+
checks the service runs (a value listed twice, an example that is not a
|
|
272
|
+
value). Compare the declared names with the keys your merge builder returns,
|
|
273
|
+
so a renamed field fails your build, not a letter.
|
|
274
|
+
- **On every deploy.** The same declaration again answers `changed: false` and
|
|
275
|
+
writes nothing; a new `app_version` alone moves `declared_at`, which is how
|
|
276
|
+
the editor tells a stale sync ("declared at commit 4d59e60, 3 days ago").
|
|
277
|
+
It creates no template version, so it never collides with someone editing
|
|
278
|
+
the template. Do not let a failed declaration fail the deploy: log it (to
|
|
279
|
+
Sentry, say) and carry on; the previous declaration stays in place.
|
|
280
|
+
- **Who may declare.** An API key with `send` or `full` scope. The dashboard
|
|
281
|
+
cannot (`forbidden`, `details.reason: "api_key_only"`): the declaration
|
|
282
|
+
speaks for your code. A declaration replaces the one before whole, from
|
|
283
|
+
whichever app sends it; the audit log records each change and names the app
|
|
284
|
+
and key it replaced.
|
|
285
|
+
- **What was really sent.** `templates.get` also returns `inputs_seen`: every
|
|
286
|
+
merge name your real sends of that template carried, with a count and when
|
|
287
|
+
first and last seen, names only, never values. `templateInputDrift(template.inputs,
|
|
288
|
+
template.inputs_seen)` lists what you declare but never send and what you
|
|
289
|
+
send but never declared.
|
|
290
|
+
|
|
200
291
|
## The dashboard variant: server-only, per signed-in person
|
|
201
292
|
|
|
202
293
|
The mail service's own dashboard (`apps/dashboard`) signs people in through
|