@wervt/nuxt 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.js +37 -1
- package/dist/provision.d.ts +12 -0
- package/dist/provision.js +35 -1
- package/dist/runtime/app/useShape.d.ts +19 -11
- package/dist/runtime/app/useShape.js +3 -8
- package/dist/runtime/email/components.d.ts +191 -0
- package/dist/runtime/email/components.js +602 -0
- package/dist/runtime/email/index.d.ts +3 -0
- package/dist/runtime/email/index.js +3 -0
- package/dist/runtime/email/render.d.ts +17 -0
- package/dist/runtime/email/render.js +68 -0
- package/dist/runtime/server/email-preview.d.ts +4 -0
- package/dist/runtime/server/email-preview.js +30 -0
- package/dist/runtime/server/index.d.ts +5 -2
- package/dist/runtime/server/index.js +3 -1
- package/dist/runtime/server/mail.d.ts +23 -0
- package/dist/runtime/server/mail.js +43 -0
- package/dist/runtime/server/shape.d.ts +12 -5
- package/dist/runtime/server/shape.js +3 -1
- package/dist/runtime/shared/shape.d.ts +18 -0
- package/dist/runtime/shared/shape.js +16 -0
- package/dist/shapes.js +67 -0
- package/package.json +7 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { renderEmail } from "../email/render.js";
|
|
2
|
+
import { createError, defineEventHandler, getQuery } from "nuxt/server";
|
|
3
|
+
import { templates } from "#wervt/emails";
|
|
4
|
+
//#region src/runtime/server/email-preview.ts
|
|
5
|
+
const list = templates;
|
|
6
|
+
const page = (body) => new Response(`<!doctype html><meta charset="utf-8"><title>Emails</title><body style="font:15px/1.6 system-ui;margin:40px">${body}</body>`, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
7
|
+
const escape = (value) => value.replace(/[&<>"]/g, (c) => ({
|
|
8
|
+
"&": "&",
|
|
9
|
+
"<": "<",
|
|
10
|
+
">": ">",
|
|
11
|
+
"\"": """
|
|
12
|
+
})[c]);
|
|
13
|
+
var email_preview_default = defineEventHandler(async (event) => {
|
|
14
|
+
const name = decodeURIComponent(new URL(event.req.url).pathname.replace(/^\/_wervt\/emails\/?/, ""));
|
|
15
|
+
if (!name) {
|
|
16
|
+
const names = Object.keys(list);
|
|
17
|
+
return page(names.length ? `<h1>Email templates</h1><ul>${names.map((n) => `<li><a href="/_wervt/emails/${n}">${escape(n)}</a> · <a href="/_wervt/emails/${n}?text">text</a></li>`).join("")}</ul>` : "<h1>No email templates</h1><p>Add <code>emails/*.vue</code> and restart <code>nuxt dev</code>.</p>");
|
|
18
|
+
}
|
|
19
|
+
const load = list[name];
|
|
20
|
+
if (!load) throw createError({
|
|
21
|
+
status: 404,
|
|
22
|
+
message: `No email template "${name}"`
|
|
23
|
+
});
|
|
24
|
+
const template = await load();
|
|
25
|
+
const mail = await renderEmail(template.default, template.previewProps);
|
|
26
|
+
if ("text" in getQuery(event)) return new Response(`Subject: ${mail.subject}\n\n${mail.text}\n`, { headers: { "content-type": "text/plain; charset=utf-8" } });
|
|
27
|
+
return new Response(mail.html, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
28
|
+
});
|
|
29
|
+
//#endregion
|
|
30
|
+
export { email_preview_default as default };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { WervtUser } from "../shared/user.js";
|
|
2
|
+
import { EmailProps, RenderedEmail, renderEmail } from "../email/render.js";
|
|
2
3
|
import { useDb } from "./db.js";
|
|
3
4
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
5
|
+
import { SendMailOptions, sendMail } from "./mail.js";
|
|
4
6
|
import { McpTokenContext } from "./mcp-auth.js";
|
|
5
|
-
import {
|
|
7
|
+
import { ShapeResponse } from "../shared/shape.js";
|
|
8
|
+
import { ShapeOptions, ShapeRow, proxyShape } from "./shape.js";
|
|
6
9
|
import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
|
|
7
10
|
import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
8
11
|
import { Tool, defineTool } from "./tools.js";
|
|
9
12
|
import { useWervtUser } from "./user.js";
|
|
10
|
-
export { type McpTokenContext, type SafeResult, type ShapeOptions, type Task, type TaskEvent, type TaskResult, type Tool, type WervtUser, createEvlogError, defineTask, defineTool, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
13
|
+
export { type EmailProps, type McpTokenContext, type RenderedEmail, type SafeResult, type SendMailOptions, type ShapeOptions, type ShapeResponse, type ShapeRow, type Task, type TaskEvent, type TaskResult, type Tool, type WervtUser, createEvlogError, defineTask, defineTool, log, proxyShape, renderEmail, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { renderEmail } from "../email/render.js";
|
|
1
2
|
import { useDb } from "./db.js";
|
|
2
3
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
4
|
+
import { sendMail } from "./mail.js";
|
|
3
5
|
import { proxyShape } from "./shape.js";
|
|
4
6
|
import { defineTask } from "./tasks.js";
|
|
5
7
|
import { defineTool } from "./tools.js";
|
|
6
8
|
import { useWervtUser } from "./user.js";
|
|
7
9
|
import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
8
|
-
export { createEvlogError, defineTask, defineTool, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
10
|
+
export { createEvlogError, defineTask, defineTool, log, proxyShape, renderEmail, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EmailProps } from "../email/render.js";
|
|
2
|
+
import { Component } from "vue";
|
|
3
|
+
//#region src/runtime/server/mail.d.ts
|
|
4
|
+
export interface SendMailOptions<T extends Component> {
|
|
5
|
+
/** One recipient. */
|
|
6
|
+
to: string;
|
|
7
|
+
/** A template from `emails/` (a component with `<WervtEmail>` at its root). */
|
|
8
|
+
template: T;
|
|
9
|
+
props?: EmailProps<T>;
|
|
10
|
+
/** Where replies go. The sender is always `<app> <mail@wervt.app>`. */
|
|
11
|
+
replyTo?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Renders a template and sends it through wervt. Throws on failure, with the status the platform
|
|
15
|
+
* answered: 400 (invalid mail), 429 (the app's or the recipient's hourly limit), 502/503.
|
|
16
|
+
*
|
|
17
|
+
* In `nuxt dev` nothing is sent: the mail is printed to the terminal, and `/_wervt/emails`
|
|
18
|
+
* previews the templates.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sendMail<T extends Component>(options: SendMailOptions<T>): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
}>;
|
|
23
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { renderEmail } from "../email/render.js";
|
|
2
|
+
import { createError } from "nuxt/server";
|
|
3
|
+
//#region src/runtime/server/mail.ts
|
|
4
|
+
/**
|
|
5
|
+
* Renders a template and sends it through wervt. Throws on failure, with the status the platform
|
|
6
|
+
* answered: 400 (invalid mail), 429 (the app's or the recipient's hourly limit), 502/503.
|
|
7
|
+
*
|
|
8
|
+
* In `nuxt dev` nothing is sent: the mail is printed to the terminal, and `/_wervt/emails`
|
|
9
|
+
* previews the templates.
|
|
10
|
+
*/
|
|
11
|
+
async function sendMail(options) {
|
|
12
|
+
const { subject, html, text } = await renderEmail(options.template, options.props);
|
|
13
|
+
const template = options.template.__name;
|
|
14
|
+
const runtime = process.env.WERVT_RUNTIME_URL;
|
|
15
|
+
if (!runtime) {
|
|
16
|
+
console.info(`[mail] to ${options.to}: ${subject}\n${text}`);
|
|
17
|
+
return { id: "dev" };
|
|
18
|
+
}
|
|
19
|
+
const res = await fetch(new URL("/_wervt/mail", runtime), {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"content-type": "application/json",
|
|
23
|
+
"x-wervt-app": process.env.WERVT_APP ?? "",
|
|
24
|
+
"x-wervt-token": process.env.WERVT_TOKEN ?? ""
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
to: options.to,
|
|
28
|
+
subject,
|
|
29
|
+
html,
|
|
30
|
+
text,
|
|
31
|
+
replyTo: options.replyTo,
|
|
32
|
+
template
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
const body = await res.json().catch(() => ({}));
|
|
36
|
+
if (!res.ok) throw createError({
|
|
37
|
+
status: res.status,
|
|
38
|
+
message: body.error ?? `sending mail failed (${res.status})`
|
|
39
|
+
});
|
|
40
|
+
return { id: body.id ?? "" };
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { sendMail };
|
|
@@ -1,19 +1,26 @@
|
|
|
1
|
+
import { ShapeResponse } from "../shared/shape.js";
|
|
1
2
|
import { RequestEvent } from "nuxt/server";
|
|
3
|
+
import { InferSelectModel, Table } from "drizzle-orm";
|
|
2
4
|
//#region src/runtime/server/shape.d.ts
|
|
3
|
-
export interface ShapeOptions {
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
export interface ShapeOptions<T extends Table | string = Table | string> {
|
|
6
|
+
/**
|
|
7
|
+
* The Drizzle table (`todos` from `server/db/schema`), or its name without schema. With the
|
|
8
|
+
* table, `useShape` knows the row type from the route.
|
|
9
|
+
*/
|
|
10
|
+
table: T;
|
|
6
11
|
/** SQL where clause. Use `$1`, `$2`, … placeholders with `params` for values. */
|
|
7
12
|
where?: string;
|
|
8
13
|
/** Values for the `$n` placeholders in `where`. */
|
|
9
14
|
params?: (string | number | boolean)[];
|
|
10
|
-
columns?: string[];
|
|
15
|
+
columns?: (T extends Table ? keyof InferSelectModel<T> & string : string)[];
|
|
11
16
|
}
|
|
17
|
+
/** The rows a shape of `T` streams: the table's select model, or untyped for a table name. */
|
|
18
|
+
export type ShapeRow<T extends Table | string> = T extends Table ? InferSelectModel<T> : Record<string, unknown>;
|
|
12
19
|
/**
|
|
13
20
|
* Serves an Electric shape from a route. Do auth checks before calling it.
|
|
14
21
|
*
|
|
15
22
|
* In production the request goes through the wervt runtime gateway, which pins the
|
|
16
23
|
* table to this app's schema. In dev it goes to Electric directly (ELECTRIC_URL).
|
|
17
24
|
*/
|
|
18
|
-
export declare function proxyShape(event: RequestEvent, { table, where, params, columns }: ShapeOptions): Promise<
|
|
25
|
+
export declare function proxyShape<T extends Table | string>(event: RequestEvent, { table: tableOrName, where, params, columns }: ShapeOptions<T>): Promise<ShapeResponse<ShapeRow<T>>>;
|
|
19
26
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getTableName, isTable } from "drizzle-orm";
|
|
1
2
|
//#region src/runtime/server/shape.ts
|
|
2
3
|
const CLIENT_PARAMS = [
|
|
3
4
|
"offset",
|
|
@@ -14,7 +15,8 @@ const CLIENT_PARAMS = [
|
|
|
14
15
|
* In production the request goes through the wervt runtime gateway, which pins the
|
|
15
16
|
* table to this app's schema. In dev it goes to Electric directly (ELECTRIC_URL).
|
|
16
17
|
*/
|
|
17
|
-
async function proxyShape(event, { table, where, params, columns }) {
|
|
18
|
+
async function proxyShape(event, { table: tableOrName, where, params, columns }) {
|
|
19
|
+
const table = isTable(tableOrName) ? getTableName(tableOrName) : tableOrName;
|
|
18
20
|
const runtime = process.env.WERVT_RUNTIME_URL;
|
|
19
21
|
const url = runtime ? new URL("/_wervt/electric/v1/shape", runtime) : new URL("/v1/shape", process.env.ELECTRIC_URL ?? "http://localhost:3000");
|
|
20
22
|
const query = new URL(event.req.url).searchParams;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/runtime/shared/shape.d.ts
|
|
2
|
+
declare const shapeRow: unique symbol;
|
|
3
|
+
export type ShapeResponse<Row> = Response & {
|
|
4
|
+
readonly [shapeRow]?: Row;
|
|
5
|
+
};
|
|
6
|
+
/** The row type of a shape route's handler, `never` if it doesn't serve a typed shape. */
|
|
7
|
+
export type ShapeRowOf<Handler> = Handler extends ((...args: never[]) => infer Result) ? Awaited<Result> extends ShapeResponse<infer Row> ? Row : never : never;
|
|
8
|
+
/**
|
|
9
|
+
* Postgres text of a `timestamp`/`timestamptz` as a Date, like Drizzle's default `mode: 'date'`
|
|
10
|
+
* reads it. Drizzle stores `timestamp` without time zone as UTC, so it's read as UTC too.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseTimestamp(value: string): Date;
|
|
13
|
+
/** Electric parsers for the types Drizzle reads differently from Electric's defaults. */
|
|
14
|
+
export declare const shapeParser: {
|
|
15
|
+
timestamp: typeof parseTimestamp;
|
|
16
|
+
timestamptz: typeof parseTimestamp;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/runtime/shared/shape.ts
|
|
2
|
+
/**
|
|
3
|
+
* Postgres text of a `timestamp`/`timestamptz` as a Date, like Drizzle's default `mode: 'date'`
|
|
4
|
+
* reads it. Drizzle stores `timestamp` without time zone as UTC, so it's read as UTC too.
|
|
5
|
+
*/
|
|
6
|
+
function parseTimestamp(value) {
|
|
7
|
+
const iso = value.replace(" ", "T").replace(/([+-]\d{2})$/, "$1:00");
|
|
8
|
+
return new Date(/(Z|[+-]\d{2}:\d{2})$/.test(iso) ? iso : `${iso}Z`);
|
|
9
|
+
}
|
|
10
|
+
/** Electric parsers for the types Drizzle reads differently from Electric's defaults. */
|
|
11
|
+
const shapeParser = {
|
|
12
|
+
timestamp: parseTimestamp,
|
|
13
|
+
timestamptz: parseTimestamp
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { parseTimestamp, shapeParser };
|
package/dist/shapes.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { extname, join, relative, sep } from "node:path";
|
|
3
|
+
//#region src/shapes.ts
|
|
4
|
+
const ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5
|
+
".ts",
|
|
6
|
+
".mts",
|
|
7
|
+
".js",
|
|
8
|
+
".mjs"
|
|
9
|
+
]);
|
|
10
|
+
/** The GET routes among `files` (relative to `server/api/shapes`), as Nitro would serve them. */
|
|
11
|
+
function shapeRoutes(files, base = "/api/shapes") {
|
|
12
|
+
const routes = [];
|
|
13
|
+
for (const file of files.toSorted((a, b) => a.localeCompare(b))) {
|
|
14
|
+
const ext = extname(file);
|
|
15
|
+
if (!ROUTE_EXTENSIONS.has(ext) || file.endsWith(`.d${ext}`)) continue;
|
|
16
|
+
const parts = file.slice(0, -ext.length).split(sep).join("/").split("/");
|
|
17
|
+
const last = parts.pop();
|
|
18
|
+
const method = last.match(/\.(get|post|put|patch|delete|head|options)$/)?.[1];
|
|
19
|
+
if (method && method !== "get") continue;
|
|
20
|
+
const name = method ? last.slice(0, -method.length - 1) : last;
|
|
21
|
+
if (name !== "index") parts.push(name);
|
|
22
|
+
let dynamic = false;
|
|
23
|
+
const segments = parts.map((segment) => {
|
|
24
|
+
if (!/^\[.+\]$/.test(segment)) return segment;
|
|
25
|
+
dynamic = true;
|
|
26
|
+
return "${string}";
|
|
27
|
+
});
|
|
28
|
+
routes.push({
|
|
29
|
+
route: [base, ...segments].join("/"),
|
|
30
|
+
dynamic,
|
|
31
|
+
file
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return routes;
|
|
35
|
+
}
|
|
36
|
+
async function findShapeRoutes(dir) {
|
|
37
|
+
return shapeRoutes(await readdir(dir, { recursive: true }).catch(() => [])).map((entry) => ({
|
|
38
|
+
...entry,
|
|
39
|
+
file: join(dir, entry.file)
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/** The `.d.ts` declaring `WervtShapeRoutes`; paths are made relative to `typesDir`. */
|
|
43
|
+
function shapeRoutesTypes(routes, typesDir, shapeTypes) {
|
|
44
|
+
const importPath = (file) => {
|
|
45
|
+
const path = relative(typesDir, file).split(sep).join("/");
|
|
46
|
+
const ext = extname(path);
|
|
47
|
+
const withoutExt = ext ? path.slice(0, -ext.length) : path;
|
|
48
|
+
return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
|
|
49
|
+
};
|
|
50
|
+
const entries = routes.map(({ route, dynamic, file }) => {
|
|
51
|
+
return ` ${dynamic ? `[path: \`${route}\`]` : JSON.stringify(route)}: ShapeRowOf<typeof import(${JSON.stringify(importPath(file))}).default>`;
|
|
52
|
+
});
|
|
53
|
+
return [
|
|
54
|
+
`import type { ShapeRowOf } from ${JSON.stringify(importPath(shapeTypes))}`,
|
|
55
|
+
"",
|
|
56
|
+
"declare global {",
|
|
57
|
+
" interface WervtShapeRoutes {",
|
|
58
|
+
...entries,
|
|
59
|
+
" }",
|
|
60
|
+
"}",
|
|
61
|
+
"",
|
|
62
|
+
"export {}",
|
|
63
|
+
""
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { findShapeRoutes, shapeRoutes, shapeRoutesTypes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wervt/nuxt",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Nuxt module for apps hosted on wervt: database, live data, MCP and runtime setup.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"drizzle",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"types": "./dist/runtime/server/index.d.ts",
|
|
34
34
|
"default": "./dist/runtime/server/index.js"
|
|
35
35
|
},
|
|
36
|
+
"./email": {
|
|
37
|
+
"types": "./dist/runtime/email/index.d.ts",
|
|
38
|
+
"default": "./dist/runtime/email/index.js"
|
|
39
|
+
},
|
|
36
40
|
"./provision": {
|
|
37
41
|
"types": "./dist/provision.d.ts",
|
|
38
42
|
"default": "./dist/provision.js"
|
|
@@ -44,11 +48,13 @@
|
|
|
44
48
|
"aws4fetch": "^1.0.20",
|
|
45
49
|
"evlog": "^2.30.1",
|
|
46
50
|
"h3-mcp": "0.2.0",
|
|
51
|
+
"html-to-text": "^9.0.5",
|
|
47
52
|
"jose": "^6.2.12",
|
|
48
53
|
"postgres": "^3.4.9",
|
|
49
54
|
"valibot": "^1.5.0"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
57
|
+
"@types/html-to-text": "^9.0.4",
|
|
52
58
|
"drizzle-orm": "1.0.0-rc.4",
|
|
53
59
|
"h3": "2.0.2",
|
|
54
60
|
"nitro": "3.0.260903-beta",
|