@wervt/nuxt 0.2.15 → 0.3.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 +2 -1
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +23 -3
- package/dist/module.js +50 -7
- package/dist/provision.d.ts +5 -1
- package/dist/provision.js +12 -1
- package/dist/route-tools.js +44 -0
- package/dist/runtime/app/useShape.d.ts +32 -11
- package/dist/runtime/app/useShape.js +104 -42
- package/dist/runtime/email/components.d.ts +71 -10
- package/dist/runtime/email/components.js +445 -98
- package/dist/runtime/email/render.js +4 -0
- package/dist/runtime/server/index.d.ts +5 -3
- package/dist/runtime/server/index.js +4 -3
- package/dist/runtime/server/mcp.js +2 -6
- package/dist/runtime/server/seed.d.ts +18 -0
- package/dist/runtime/server/seed.js +27 -0
- package/dist/runtime/server/shape.d.ts +12 -5
- package/dist/runtime/server/shape.js +3 -1
- package/dist/runtime/server/tool-handlers.d.ts +5 -0
- package/dist/runtime/server/tool-handlers.js +37 -13
- package/dist/runtime/server/tools.d.ts +36 -0
- package/dist/runtime/server/tools.js +53 -1
- package/dist/runtime/server/validate.d.ts +1 -0
- package/dist/runtime/server/validate.js +1 -1
- package/dist/runtime/shared/shape.d.ts +18 -0
- package/dist/runtime/shared/shape.js +16 -0
- package/dist/seed.js +52 -0
- package/dist/shapes.js +70 -0
- package/package.json +1 -1
|
@@ -2,9 +2,10 @@ import { renderEmail } from "../email/render.js";
|
|
|
2
2
|
import { useDb } from "./db.js";
|
|
3
3
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
4
4
|
import { sendMail } from "./mail.js";
|
|
5
|
+
import { seedTable } from "./seed.js";
|
|
5
6
|
import { proxyShape } from "./shape.js";
|
|
6
7
|
import { defineTask } from "./tasks.js";
|
|
7
|
-
import { defineTool } from "./tools.js";
|
|
8
|
-
import { useWervtUser } from "./user.js";
|
|
9
8
|
import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
10
|
-
|
|
9
|
+
import { defineTool, defineToolRoute } from "./tools.js";
|
|
10
|
+
import { useWervtUser } from "./user.js";
|
|
11
|
+
export { createEvlogError, defineTask, defineTool, defineToolRoute, log, proxyShape, renderEmail, seedTable, sendMail, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
@@ -2,10 +2,9 @@ import { devUser } from "../shared/user.js";
|
|
|
2
2
|
import { useLogger } from "./logger.js";
|
|
3
3
|
import { safeValidate } from "./validate.js";
|
|
4
4
|
import { authenticateMcp, mcpEndpoint, setMcpUser } from "./mcp-auth.js";
|
|
5
|
-
import {
|
|
5
|
+
import { allTools, toolInputSchema } from "./tool-handlers.js";
|
|
6
6
|
import { defineEventHandler } from "nuxt/server";
|
|
7
7
|
import { server } from "#wervt/mcp";
|
|
8
|
-
import { tools } from "#wervt/tools";
|
|
9
8
|
import { defineMcpHandler } from "h3-mcp";
|
|
10
9
|
//#region src/runtime/server/mcp.ts
|
|
11
10
|
const MAX_BODY = 4194304;
|
|
@@ -56,10 +55,7 @@ function toMcpTool(name, tool) {
|
|
|
56
55
|
}
|
|
57
56
|
let handler;
|
|
58
57
|
async function createHandler() {
|
|
59
|
-
const definitions =
|
|
60
|
-
const tool = await loadTool(name);
|
|
61
|
-
return tool ? toMcpTool(name, tool) : void 0;
|
|
62
|
-
}))).filter((tool) => tool !== void 0);
|
|
58
|
+
const definitions = [...await allTools()].map(([name, tool]) => toMcpTool(name, tool));
|
|
63
59
|
return defineMcpHandler((event) => ({
|
|
64
60
|
...server,
|
|
65
61
|
tools: definitions,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { InferInsertModel } from "drizzle-orm";
|
|
2
|
+
import { PgTable } from "drizzle-orm/pg-core";
|
|
3
|
+
//#region src/runtime/server/seed.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Inserts `rows` into `table` only while it is empty, so a seed task can run again without
|
|
6
|
+
* duplicating data or touching what users created. Returns how many rows it inserted.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // server/tasks/db/seed.ts
|
|
10
|
+
* export default defineTask({
|
|
11
|
+
* meta: { description: 'Fill an empty database with example data' },
|
|
12
|
+
* async run() {
|
|
13
|
+
* return { result: { todos: await seedTable(todos, [{ title: 'Try wervt' }]) } }
|
|
14
|
+
* },
|
|
15
|
+
* })
|
|
16
|
+
*/
|
|
17
|
+
export declare function seedTable<T extends PgTable>(table: T, rows: InferInsertModel<T>[]): Promise<number>;
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useDb } from "./db.js";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
//#region src/runtime/server/seed.ts
|
|
4
|
+
/**
|
|
5
|
+
* Inserts `rows` into `table` only while it is empty, so a seed task can run again without
|
|
6
|
+
* duplicating data or touching what users created. Returns how many rows it inserted.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // server/tasks/db/seed.ts
|
|
10
|
+
* export default defineTask({
|
|
11
|
+
* meta: { description: 'Fill an empty database with example data' },
|
|
12
|
+
* async run() {
|
|
13
|
+
* return { result: { todos: await seedTable(todos, [{ title: 'Try wervt' }]) } }
|
|
14
|
+
* },
|
|
15
|
+
* })
|
|
16
|
+
*/
|
|
17
|
+
async function seedTable(table, rows) {
|
|
18
|
+
if (rows.length === 0) return 0;
|
|
19
|
+
const db = useDb();
|
|
20
|
+
const target = table;
|
|
21
|
+
const [existing] = await db.select({ one: sql`1` }).from(target).limit(1);
|
|
22
|
+
if (existing) return 0;
|
|
23
|
+
await db.insert(target).values(rows);
|
|
24
|
+
return rows.length;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { seedTable };
|
|
@@ -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;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Input } from "./validate.js";
|
|
2
2
|
import { Tool } from "./tools.js";
|
|
3
3
|
//#region src/runtime/server/tool-handlers.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Every tool by the name agents see. A tool may rename itself (`name`), so the file-derived
|
|
6
|
+
* keys of `#wervt/tools` are only a default and the whole list is loaded once.
|
|
7
|
+
*/
|
|
8
|
+
export declare function allTools(): Promise<Map<string, Tool<Input | undefined, unknown>>>;
|
|
4
9
|
export declare function loadTool(name: string): Promise<Tool<Input | undefined, unknown> | undefined>;
|
|
5
10
|
export declare function toolInputSchema(tool: Tool): import("@valibot/to-json-schema").JsonSchema | {
|
|
6
11
|
type: string;
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { useLogger } from "./logger.js";
|
|
2
2
|
import { safeValidate, toSchema } from "./validate.js";
|
|
3
|
+
import { TOOL_ROUTE } from "./tools.js";
|
|
3
4
|
import { defineEventHandler } from "nuxt/server";
|
|
4
|
-
import { tools } from "#wervt/tools";
|
|
5
5
|
import { toJsonSchema } from "@valibot/to-json-schema";
|
|
6
|
+
import { routeMethods, tools } from "#wervt/tools";
|
|
6
7
|
//#region src/runtime/server/tool-handlers.ts
|
|
8
|
+
let index;
|
|
9
|
+
/**
|
|
10
|
+
* Every tool by the name agents see. A tool may rename itself (`name`), so the file-derived
|
|
11
|
+
* keys of `#wervt/tools` are only a default and the whole list is loaded once.
|
|
12
|
+
*/
|
|
13
|
+
function allTools() {
|
|
14
|
+
index ??= (async () => {
|
|
15
|
+
const byName = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const [key, load] of Object.entries(tools)) {
|
|
17
|
+
const exported = (await load())?.default;
|
|
18
|
+
const tool = exported?.[TOOL_ROUTE] ?? exported;
|
|
19
|
+
if (!tool) continue;
|
|
20
|
+
const method = Object.hasOwn(routeMethods, key) ? routeMethods[key] : void 0;
|
|
21
|
+
const name = tool.name ?? key;
|
|
22
|
+
if (byName.has(name)) console.warn(`[wervt] Two tools are named ${name}; the last one wins.`);
|
|
23
|
+
byName.set(name, method ? {
|
|
24
|
+
...tool,
|
|
25
|
+
readOnly: tool.readOnly ?? method === "get"
|
|
26
|
+
} : tool);
|
|
27
|
+
}
|
|
28
|
+
return byName;
|
|
29
|
+
})().catch((error) => {
|
|
30
|
+
index = void 0;
|
|
31
|
+
throw error;
|
|
32
|
+
});
|
|
33
|
+
return index;
|
|
34
|
+
}
|
|
7
35
|
async function loadTool(name) {
|
|
8
|
-
return (await (
|
|
36
|
+
return (await allTools()).get(name);
|
|
9
37
|
}
|
|
10
38
|
function toolInputSchema(tool) {
|
|
11
39
|
if (!tool.input) return {
|
|
@@ -22,16 +50,12 @@ function toolInputSchema(tool) {
|
|
|
22
50
|
/** `GET /_wervt/tools`: name, description and JSON Schema of each tool, for the client plugin. */
|
|
23
51
|
const listTools = defineEventHandler(async (event) => {
|
|
24
52
|
event.res.headers.set("cache-control", "no-store");
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
inputSchema: toolInputSchema(tool),
|
|
32
|
-
readOnly: tool.readOnly === true
|
|
33
|
-
};
|
|
34
|
-
}))).filter((tool) => tool !== void 0);
|
|
53
|
+
return [...await allTools()].map(([name, tool]) => ({
|
|
54
|
+
name,
|
|
55
|
+
description: tool.description,
|
|
56
|
+
inputSchema: toolInputSchema(tool),
|
|
57
|
+
readOnly: tool.readOnly === true
|
|
58
|
+
}));
|
|
35
59
|
});
|
|
36
60
|
function crossSite(event) {
|
|
37
61
|
const site = event.req.headers.get("sec-fetch-site");
|
|
@@ -67,4 +91,4 @@ const runTool = defineEventHandler(async (event) => {
|
|
|
67
91
|
return { result: await tool.handler(event, input) ?? null };
|
|
68
92
|
});
|
|
69
93
|
//#endregion
|
|
70
|
-
export { listTools, loadTool, runTool, toolInputSchema };
|
|
94
|
+
export { allTools, listTools, loadTool, runTool, toolInputSchema };
|
|
@@ -2,6 +2,8 @@ import { Input, Output } from "./validate.js";
|
|
|
2
2
|
import { RequestEvent } from "nuxt/server";
|
|
3
3
|
//#region src/runtime/server/tools.d.ts
|
|
4
4
|
export interface Tool<I extends Input | undefined = Input | undefined, R = unknown> {
|
|
5
|
+
/** Overrides the name from the file path. Agents see it, so keep it short and stable. */
|
|
6
|
+
name?: string;
|
|
5
7
|
/** What the tool does and when an agent should call it. Agents read this to pick a tool. */
|
|
6
8
|
description: string;
|
|
7
9
|
/** valibot schema or `{ key: schema }` shape of the arguments. Omit it for no arguments. */
|
|
@@ -12,4 +14,38 @@ export interface Tool<I extends Input | undefined = Input | undefined, R = unkno
|
|
|
12
14
|
}
|
|
13
15
|
/** A tool in `server/tools/` (`server/tools/todos/add.ts` is `todos.add`). */
|
|
14
16
|
export declare function defineTool<R, I extends Input | undefined = undefined>(tool: Tool<I, R>): Tool<I, R>;
|
|
17
|
+
type Values<T extends Input | undefined> = T extends Input ? Output<T> : {};
|
|
18
|
+
export interface ToolRoute<P extends Input | undefined, I extends Input | undefined, R> {
|
|
19
|
+
/** How agents see the route. `readOnly` defaults to true for GET routes. */
|
|
20
|
+
tool: {
|
|
21
|
+
name?: string;
|
|
22
|
+
description: string;
|
|
23
|
+
readOnly?: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** Route params like `[id]` (strings on the route; use `vh.*` to coerce). */
|
|
26
|
+
params?: P;
|
|
27
|
+
/** The body, or the query for GET routes. */
|
|
28
|
+
input?: I;
|
|
29
|
+
/** Gets params and input merged, from the request or from an agent's arguments. */
|
|
30
|
+
handler(event: RequestEvent, input: Values<P> & Values<I>): R | Promise<R>;
|
|
31
|
+
}
|
|
32
|
+
/** Where `defineToolRoute` keeps the tool on the event handler, for the module's tool list. */
|
|
33
|
+
export declare const TOOL_ROUTE: unique symbol;
|
|
34
|
+
/**
|
|
35
|
+
* An API route that agents can call as a tool too, over WebMCP and `/mcp`: no separate file in
|
|
36
|
+
* `server/tools/` repeating the route's logic and validation.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* // server/api/todos/[id].patch.ts
|
|
40
|
+
* export default defineToolRoute({
|
|
41
|
+
* tool: { name: 'todos.update', description: 'Mark a todo done or not done.' },
|
|
42
|
+
* params: { id: vh.id },
|
|
43
|
+
* input: { done: v.boolean() },
|
|
44
|
+
* handler: (event, { id, done }) => …,
|
|
45
|
+
* })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function defineToolRoute<R, P extends Input | undefined = undefined, I extends Input | undefined = undefined>(route: ToolRoute<P, I, R>): import("nuxt/server").EventHandler<Promise<R>> & {
|
|
49
|
+
[TOOL_ROUTE]: Tool<Input | undefined, unknown>;
|
|
50
|
+
};
|
|
15
51
|
//#endregion
|
|
@@ -1,7 +1,59 @@
|
|
|
1
|
+
import { validate } from "./validate.js";
|
|
2
|
+
import { defineEventHandler, getQuery, getRouterParams, readBody } from "nuxt/server";
|
|
3
|
+
import * as v from "valibot";
|
|
1
4
|
//#region src/runtime/server/tools.ts
|
|
2
5
|
/** A tool in `server/tools/` (`server/tools/todos/add.ts` is `todos.add`). */
|
|
3
6
|
function defineTool(tool) {
|
|
4
7
|
return tool;
|
|
5
8
|
}
|
|
9
|
+
/** Where `defineToolRoute` keeps the tool on the event handler, for the module's tool list. */
|
|
10
|
+
const TOOL_ROUTE = Symbol.for("wervt.toolRoute");
|
|
11
|
+
/**
|
|
12
|
+
* An API route that agents can call as a tool too, over WebMCP and `/mcp`: no separate file in
|
|
13
|
+
* `server/tools/` repeating the route's logic and validation.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* // server/api/todos/[id].patch.ts
|
|
17
|
+
* export default defineToolRoute({
|
|
18
|
+
* tool: { name: 'todos.update', description: 'Mark a todo done or not done.' },
|
|
19
|
+
* params: { id: vh.id },
|
|
20
|
+
* input: { done: v.boolean() },
|
|
21
|
+
* handler: (event, { id, done }) => …,
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function defineToolRoute(route) {
|
|
26
|
+
const handler = defineEventHandler(async (event) => {
|
|
27
|
+
const params = route.params ? await validate(route.params, getRouterParams(event, { decode: true })) : {};
|
|
28
|
+
const reads = event.req.method === "GET" || event.req.method === "HEAD";
|
|
29
|
+
const input = route.input ? await validate(route.input, reads ? getQuery(event) : await readBody(event) ?? {}) : {};
|
|
30
|
+
return route.handler(event, {
|
|
31
|
+
...params,
|
|
32
|
+
...input
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
const tool = {
|
|
36
|
+
name: route.tool.name,
|
|
37
|
+
description: route.tool.description,
|
|
38
|
+
readOnly: route.tool.readOnly,
|
|
39
|
+
input: toolInput(route.params, route.input),
|
|
40
|
+
handler: (event, input) => route.handler(event, input ?? {})
|
|
41
|
+
};
|
|
42
|
+
return Object.assign(handler, { [TOOL_ROUTE]: tool });
|
|
43
|
+
}
|
|
44
|
+
/** An agent sends params and input as one object, so the tool's schema is both merged. */
|
|
45
|
+
function toolInput(params, input) {
|
|
46
|
+
if (!params) return input;
|
|
47
|
+
if (!input) return params;
|
|
48
|
+
return v.objectAsync({
|
|
49
|
+
...entries(params),
|
|
50
|
+
...entries(input)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function entries(input) {
|
|
54
|
+
if (!("kind" in input && input.kind === "schema")) return input;
|
|
55
|
+
if ("entries" in input) return input.entries;
|
|
56
|
+
throw new Error("defineToolRoute: `params` and `input` must be objects to merge into a tool.");
|
|
57
|
+
}
|
|
6
58
|
//#endregion
|
|
7
|
-
export { defineTool };
|
|
59
|
+
export { TOOL_ROUTE, defineTool, defineToolRoute };
|
|
@@ -16,6 +16,7 @@ export type SafeResult<T extends Input> = {
|
|
|
16
16
|
};
|
|
17
17
|
export declare function toSchema(input: Input): Schema;
|
|
18
18
|
export declare function safeValidate<T extends Input>(input: T, data: unknown): Promise<SafeResult<T>>;
|
|
19
|
+
export declare function validate<T extends Input>(input: T, data: unknown): Promise<Output<T>>;
|
|
19
20
|
/** Parsed JSON body. Throws a 400 with flattened issues when invalid. */
|
|
20
21
|
export declare const useValidatedBody: <T extends Input>(event: RequestEvent, schema: T) => Promise<Output<T>>;
|
|
21
22
|
/** Query parameters (strings; use `vh.*` to coerce). Throws a 400 when invalid. */
|
|
@@ -48,4 +48,4 @@ const vh = {
|
|
|
48
48
|
id: v.pipe(v.string(), v.digits(), v.transform(Number), v.safeInteger(), v.minValue(1))
|
|
49
49
|
};
|
|
50
50
|
//#endregion
|
|
51
|
-
export { safeValidate, toSchema, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh };
|
|
51
|
+
export { safeValidate, toSchema, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, validate, vh };
|
|
@@ -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/seed.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/seed.ts
|
|
2
|
+
/** The conventional seed task: `server/tasks/db/seed.ts`. */
|
|
3
|
+
const SEED_TASK = "db:seed";
|
|
4
|
+
/**
|
|
5
|
+
* `nitro.scheduledTasks` without the seed task, and the crons it was listed under. Seeding fills
|
|
6
|
+
* an empty database on request; on a schedule it would run against production data.
|
|
7
|
+
*/
|
|
8
|
+
function unscheduleSeed(schedules) {
|
|
9
|
+
const kept = {};
|
|
10
|
+
const removed = [];
|
|
11
|
+
for (const [cron, entry] of Object.entries(schedules)) {
|
|
12
|
+
const tasks = [entry].flat();
|
|
13
|
+
const rest = tasks.filter((task) => task !== SEED_TASK);
|
|
14
|
+
if (rest.length < tasks.length) removed.push(cron);
|
|
15
|
+
if (rest.length > 0) kept[cron] = Array.isArray(entry) ? rest : rest[0];
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
schedules: kept,
|
|
19
|
+
removed
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs `db:seed` through the Nitro dev server's task route, once the server answers. The dev
|
|
24
|
+
* server is still building when it starts listening, so its task list is polled first; that
|
|
25
|
+
* also tells whether the app has a seed task at all.
|
|
26
|
+
*/
|
|
27
|
+
async function runDevSeed(baseUrl, { fetch: request = fetch, timeout = 12e4, interval = 500 } = {}) {
|
|
28
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
29
|
+
const deadline = Date.now() + timeout;
|
|
30
|
+
let tasks;
|
|
31
|
+
while (!tasks) {
|
|
32
|
+
const response = await request(`${base}/_nitro/tasks`).catch(() => void 0);
|
|
33
|
+
const body = response?.ok ? await response.json().catch(() => void 0) : void 0;
|
|
34
|
+
if (body && typeof body === "object" && "tasks" in body) tasks = body.tasks;
|
|
35
|
+
else if (Date.now() >= deadline) throw new Error("the dev server did not answer in time");
|
|
36
|
+
else await new Promise((resolve) => setTimeout(resolve, interval));
|
|
37
|
+
}
|
|
38
|
+
if (!("db:seed" in tasks)) return { status: "missing" };
|
|
39
|
+
const response = await request(`${base}/_nitro/tasks/${SEED_TASK}`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
body: JSON.stringify({ payload: {} })
|
|
43
|
+
});
|
|
44
|
+
const body = await response.json().catch(() => ({}));
|
|
45
|
+
if (!response.ok) throw new Error(body.message ?? `${response.status} ${response.statusText}`);
|
|
46
|
+
return {
|
|
47
|
+
status: "seeded",
|
|
48
|
+
result: body.result
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { SEED_TASK, runDevSeed, unscheduleSeed };
|
package/dist/shapes.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
function shapesManifest(routes) {
|
|
43
|
+
return { shapes: routes.filter((route) => !route.dynamic).map((route) => route.route) };
|
|
44
|
+
}
|
|
45
|
+
/** The `.d.ts` declaring `WervtShapeRoutes`; paths are made relative to `typesDir`. */
|
|
46
|
+
function shapeRoutesTypes(routes, typesDir, shapeTypes) {
|
|
47
|
+
const importPath = (file) => {
|
|
48
|
+
const path = relative(typesDir, file).split(sep).join("/");
|
|
49
|
+
const ext = extname(path);
|
|
50
|
+
const withoutExt = ext ? path.slice(0, -ext.length) : path;
|
|
51
|
+
return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
|
|
52
|
+
};
|
|
53
|
+
const entries = routes.map(({ route, dynamic, file }) => {
|
|
54
|
+
return ` ${dynamic ? `[path: \`${route}\`]` : JSON.stringify(route)}: ShapeRowOf<typeof import(${JSON.stringify(importPath(file))}).default>`;
|
|
55
|
+
});
|
|
56
|
+
return [
|
|
57
|
+
`import type { ShapeRowOf } from ${JSON.stringify(importPath(shapeTypes))}`,
|
|
58
|
+
"",
|
|
59
|
+
"declare global {",
|
|
60
|
+
" interface WervtShapeRoutes {",
|
|
61
|
+
...entries,
|
|
62
|
+
" }",
|
|
63
|
+
"}",
|
|
64
|
+
"",
|
|
65
|
+
"export {}",
|
|
66
|
+
""
|
|
67
|
+
].join("\n");
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { findShapeRoutes, shapeRoutes, shapeRoutesTypes, shapesManifest };
|