@wervt/nuxt 0.3.2 → 0.3.3
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 -0
- package/dist/dev.d.ts +3 -1
- package/dist/env-types.js +14 -0
- package/dist/icons.js +31 -0
- package/dist/module.js +39 -0
- package/dist/node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/task.js +1 -2
- package/dist/og-image.js +39 -0
- package/dist/provision.d.ts +19 -2
- package/dist/provision.js +49 -2
- package/dist/runtime/app/snapshot.d.ts +17 -0
- package/dist/runtime/app/snapshot.js +64 -0
- package/dist/runtime/app/useShape.d.ts +8 -1
- package/dist/runtime/app/useShape.js +8 -52
- package/dist/runtime/app/useWervtFeedback.d.ts +2 -1
- package/dist/runtime/email/index.d.ts +2 -1
- package/dist/runtime/email/index.js +2 -1
- package/dist/runtime/email/invite.d.ts +75 -0
- package/dist/runtime/email/invite.js +60 -0
- package/dist/runtime/server/access.d.ts +3 -2
- package/dist/runtime/server/access.js +3 -2
- package/dist/runtime/server/agent-token-plugin.d.ts +13 -0
- package/dist/runtime/server/agent-token-plugin.js +10 -0
- package/dist/runtime/server/anthropic.d.ts +21 -0
- package/dist/runtime/server/anthropic.js +53 -0
- package/dist/runtime/server/identity.d.ts +20 -0
- package/dist/runtime/server/identity.js +37 -0
- package/dist/runtime/server/index.d.ts +4 -3
- package/dist/runtime/server/index.js +4 -3
- package/dist/runtime/server/task-handlers.d.ts +2 -1
- package/dist/runtime/server/tools.d.ts +2 -1
- package/dist/runtime/server/user.d.ts +9 -3
- package/dist/runtime/server/user.js +28 -3
- package/dist/runtime/server/validate.d.ts +2 -1
- package/dist/runtime/shared/agent-token.d.ts +9 -0
- package/dist/runtime/shared/agent-token.js +17 -0
- package/dist/runtime/shared/shape.d.ts +2 -1
- package/dist/runtime/shared/user.d.ts +8 -0
- package/dist/runtime/shared/user.js +4 -1
- package/dist/storage.d.ts +48 -1
- package/dist/storage.js +217 -3
- package/package.json +15 -6
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ export default defineNuxtConfig({
|
|
|
20
20
|
- `useWervtUser()`: the signed-in wervt user, on the server and in the app.
|
|
21
21
|
- `useWervtAccess(event)`: invites on invite-only apps (`wervt access <app> --invite`), as the
|
|
22
22
|
signed-in user.
|
|
23
|
+
- `useAnthropic()` (`@wervt/nuxt/anthropic`): an Anthropic client without an API key, through the
|
|
24
|
+
app's wervt identity (`wervt identity enable <app>`); `useWorkloadIdentity()` for the raw token.
|
|
23
25
|
- `useValidatedBody()` / `useValidatedQuery()` / `useValidatedParams()`: valibot validation.
|
|
24
26
|
- `useWervtTrack()`: custom events for wervt's cookieless analytics (page views are counted automatically).
|
|
25
27
|
- `wervt: { feedback: { key } }` and `useWervtFeedback()`: the kontor feedback widget, reports go into a kontor project's triage.
|
package/dist/dev.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WervtRole } from "./runtime/shared/user.js";
|
|
1
2
|
import "nuxt/schema";
|
|
2
3
|
//#region src/dev.d.ts
|
|
3
4
|
export interface DevOptions {
|
|
@@ -18,7 +19,7 @@ export interface DevOptions {
|
|
|
18
19
|
seed?: boolean;
|
|
19
20
|
/**
|
|
20
21
|
* Who `/_wervt/login` signs in as during `nuxt dev` (there's no wervt auth locally).
|
|
21
|
-
* Defaults to a dev user with a Twitch account linked.
|
|
22
|
+
* Defaults to a dev user with a Twitch account linked. Without `role`, they're the app's owner.
|
|
22
23
|
*/
|
|
23
24
|
user?: {
|
|
24
25
|
id: string;
|
|
@@ -26,6 +27,7 @@ export interface DevOptions {
|
|
|
26
27
|
name?: string;
|
|
27
28
|
image?: string;
|
|
28
29
|
accounts?: Record<string, string>;
|
|
30
|
+
role?: WervtRole;
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
//#endregion
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/env-types.ts
|
|
2
|
+
/** `.nuxt/types/wervt-env.d.ts`: types for the `process.env` the isolate provides. */
|
|
3
|
+
const PROCESS_ENV_TYPES = `declare namespace NodeJS {
|
|
4
|
+
interface ProcessEnv {
|
|
5
|
+
[key: string]: string | undefined
|
|
6
|
+
}
|
|
7
|
+
interface Process {
|
|
8
|
+
env: ProcessEnv
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
declare var process: NodeJS.Process
|
|
12
|
+
`;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { PROCESS_ENV_TYPES };
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { dependencyNames } from "./og-image.js";
|
|
2
|
+
//#region src/icons.ts
|
|
3
|
+
const ICONIFY_JSON = "@iconify-json/";
|
|
4
|
+
/** The whole Iconify set in one package; @nuxt/icon finds every collection in it. */
|
|
5
|
+
const ICONIFY_ALL = "@iconify/json";
|
|
6
|
+
/** Where the app's icons can come from without the Iconify API. */
|
|
7
|
+
async function iconSources(rootDir) {
|
|
8
|
+
const names = await dependencyNames(rootDir);
|
|
9
|
+
return {
|
|
10
|
+
collections: names.filter((name) => name.startsWith(ICONIFY_JSON)).map((name) => name.slice(14)).toSorted(),
|
|
11
|
+
everything: names.includes(ICONIFY_ALL)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @nuxt/icon defaults for a wervt build. Its `serverBundle: 'auto'` already resolves to the
|
|
16
|
+
* locally installed collections on the Deno preset (auto-discovery also covers layers and
|
|
17
|
+
* transitive deps), so only the API fallback needs changing: the isolate can't reach the Iconify
|
|
18
|
+
* API, and a server request would time out for every icon of a missing collection. The browser
|
|
19
|
+
* may still fall back. An app's own `icon` config wins over these.
|
|
20
|
+
*/
|
|
21
|
+
function iconDefaults() {
|
|
22
|
+
return { fallbackToApi: "client-only" };
|
|
23
|
+
}
|
|
24
|
+
/** Why icons won't render on wervt, if they won't. Nuxt UI's own icons are Lucide icons. */
|
|
25
|
+
function iconWarning(sources, nuxtUi) {
|
|
26
|
+
if (sources.everything) return void 0;
|
|
27
|
+
if (nuxtUi && !sources.collections.includes("lucide")) return "Nuxt UI uses Lucide icons, but `@iconify-json/lucide` is not installed, so they won't render on wervt (the isolate can't reach the Iconify API). Run `pnpm add -D @iconify-json/lucide`, plus `@iconify-json/<name>` for every other icon collection the app uses.";
|
|
28
|
+
if (sources.collections.length === 0) return "No icon collection is installed, so icons won't render on wervt (the isolate can't reach the Iconify API). Install the collections the app uses, e.g. `pnpm add -D @iconify-json/lucide`.";
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { iconDefaults, iconSources, iconWarning };
|
package/dist/module.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { SEED_TASK, unscheduleSeed } from "./seed.js";
|
|
2
2
|
import { resolveAppName, setupDev } from "./dev.js";
|
|
3
|
+
import { PROCESS_ENV_TYPES } from "./env-types.js";
|
|
4
|
+
import { dependencyNames, ogImageDefaults, ogImageWarning } from "./og-image.js";
|
|
5
|
+
import { iconDefaults, iconSources, iconWarning } from "./icons.js";
|
|
3
6
|
import { findRouteTools } from "./route-tools.js";
|
|
4
7
|
import { localFiles, previewManifest } from "./preview.js";
|
|
5
8
|
import { findShapeRoutes, shapeRoutesTypes, shapesManifest } from "./shapes.js";
|
|
@@ -19,6 +22,19 @@ var module_default = defineNuxtModule({
|
|
|
19
22
|
name: "@wervt/nuxt",
|
|
20
23
|
configKey: "wervt"
|
|
21
24
|
},
|
|
25
|
+
async moduleDependencies(nuxt) {
|
|
26
|
+
if (nuxt.options.dev) return {};
|
|
27
|
+
return {
|
|
28
|
+
"@nuxt/icon": {
|
|
29
|
+
optional: true,
|
|
30
|
+
defaults: iconDefaults()
|
|
31
|
+
},
|
|
32
|
+
"nuxt-og-image": {
|
|
33
|
+
optional: true,
|
|
34
|
+
defaults: ogImageDefaults()
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
},
|
|
22
38
|
async setup(options, nuxt) {
|
|
23
39
|
const resolver = createResolver(import.meta.url);
|
|
24
40
|
const nitro = nuxt.options.nitro;
|
|
@@ -51,6 +67,7 @@ var module_default = defineNuxtModule({
|
|
|
51
67
|
});
|
|
52
68
|
}
|
|
53
69
|
}
|
|
70
|
+
addServerPlugin(resolver.resolve("./runtime/server/agent-token-plugin"));
|
|
54
71
|
const useContentDatabase = !nuxt.options.dev && hasNuxtModule("@nuxt/content", nuxt);
|
|
55
72
|
if (useContentDatabase) {
|
|
56
73
|
const options = nuxt.options;
|
|
@@ -123,6 +140,16 @@ var module_default = defineNuxtModule({
|
|
|
123
140
|
};
|
|
124
141
|
});
|
|
125
142
|
}
|
|
143
|
+
const nuxtUi = hasNuxtModule("@nuxt/ui", nuxt);
|
|
144
|
+
if (!nuxt.options.dev && !nuxt.options._prepare && (nuxtUi || hasNuxtModule("@nuxt/icon", nuxt))) {
|
|
145
|
+
const warning = iconWarning(await iconSources(nuxt.options.rootDir), nuxtUi);
|
|
146
|
+
if (warning) logger.warn(warning);
|
|
147
|
+
}
|
|
148
|
+
nuxt.hook("modules:done", async () => {
|
|
149
|
+
if (nuxt.options.dev || nuxt.options._prepare || !hasNuxtModule("nuxt-og-image", nuxt)) return;
|
|
150
|
+
const warning = ogImageWarning(await dependencyNames(nuxt.options.rootDir));
|
|
151
|
+
if (warning) logger.warn(warning);
|
|
152
|
+
});
|
|
126
153
|
addServerHandler({
|
|
127
154
|
route: "/_wervt/boot",
|
|
128
155
|
method: "get",
|
|
@@ -256,6 +283,18 @@ var module_default = defineNuxtModule({
|
|
|
256
283
|
filename: "types/wervt-email.d.ts",
|
|
257
284
|
getContents: () => "declare module '*.vue' {\n import type { DefineComponent } from 'vue'\n const component: DefineComponent<Record<string, unknown>>\n export default component\n}\n"
|
|
258
285
|
}, { nitro: true });
|
|
286
|
+
addTypeTemplate({
|
|
287
|
+
filename: "types/wervt-env.d.ts",
|
|
288
|
+
getContents: () => PROCESS_ENV_TYPES
|
|
289
|
+
}, { nitro: true });
|
|
290
|
+
const sourceEnv = resolver.resolve("./env.d.ts");
|
|
291
|
+
if (existsSync(sourceEnv)) addTypeTemplate({
|
|
292
|
+
filename: "types/wervt-source-env.d.ts",
|
|
293
|
+
getContents: () => `/// <reference path=${JSON.stringify(sourceEnv)} />\n`
|
|
294
|
+
}, {
|
|
295
|
+
nitro: true,
|
|
296
|
+
nuxt: true
|
|
297
|
+
});
|
|
259
298
|
if (nuxt.options.dev) {
|
|
260
299
|
const templates = await findEmailTemplates([join(nuxt.options.srcDir, "emails"), join(nuxt.options.rootDir, "emails")]);
|
|
261
300
|
addServerTemplate({
|
package/dist/node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/task.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { runTask };
|
|
1
|
+
export { runTask } from "./internal/task.js";
|
package/dist/og-image.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
//#region src/og-image.ts
|
|
4
|
+
/** The app's dependencies and devDependencies, by name. */
|
|
5
|
+
async function dependencyNames(rootDir) {
|
|
6
|
+
const pkg = JSON.parse(await readFile(join(rootDir, "package.json"), "utf8").catch(() => "{}"));
|
|
7
|
+
return Object.keys({
|
|
8
|
+
...pkg.dependencies,
|
|
9
|
+
...pkg.devDependencies
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* nuxt-og-image defaults for a wervt build. It treats the deno-server preset like Node and picks
|
|
14
|
+
* the native renderers (N-API bindings, sharp, a local Chrome), which an Edge Runtime isolate
|
|
15
|
+
* can't load, so OG image requests fail with a 500. The WebAssembly builds run there; without
|
|
16
|
+
* sharp, images are PNG only. An app's own `ogImage` config wins over these.
|
|
17
|
+
*/
|
|
18
|
+
function ogImageDefaults() {
|
|
19
|
+
return { compatibility: { runtime: {
|
|
20
|
+
takumi: "wasm",
|
|
21
|
+
satori: "wasm",
|
|
22
|
+
resvg: "wasm",
|
|
23
|
+
sharp: false,
|
|
24
|
+
browser: false
|
|
25
|
+
} } };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Why OG images won't render on wervt, if they won't. The WebAssembly package renders; the native
|
|
29
|
+
* one is needed too because nuxt-og-image checks for the packages of the preset's own defaults
|
|
30
|
+
* (Node for deno-server) and fails the build without them, whatever `compatibility` says.
|
|
31
|
+
*/
|
|
32
|
+
function ogImageWarning(dependencies) {
|
|
33
|
+
const has = (name) => dependencies.includes(name);
|
|
34
|
+
const missing = [...has("@takumi-rs/core") || has("@takumi-rs/wasm") || !has("satori") ? ["@takumi-rs/core", "@takumi-rs/wasm"] : [], ...has("satori") ? ["@resvg/resvg-js", "@resvg/resvg-wasm"] : []].filter((name) => !has(name));
|
|
35
|
+
if (missing.length === 0) return void 0;
|
|
36
|
+
return `OG images render with WebAssembly on wervt (an isolate can't load native renderers), and nuxt-og-image's build also wants the native package, but ${missing.map((name) => `\`${name}\``).join(", ")} ${missing.length > 1 ? "are" : "is"} not installed. Run \`pnpm add ${missing.join(" ")}\`.`;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { dependencyNames, ogImageDefaults, ogImageWarning };
|
package/dist/provision.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StorageError, StorageOptions, StorageUsage, createStorage } from "./storage.js";
|
|
1
|
+
import { MAX_KEY_BYTES, ObjectDetails, ObjectListing, StorageError, StorageOptions, StorageQuota, StorageUsage, StoredObject, createStorage } from "./storage.js";
|
|
2
2
|
//#region src/provision.d.ts
|
|
3
3
|
export interface ProvisionOptions {
|
|
4
4
|
/** App name, `[a-z0-9-]+`. */
|
|
@@ -9,7 +9,24 @@ export interface ProvisionOptions {
|
|
|
9
9
|
secret: string;
|
|
10
10
|
/** Drizzle migrations folder. Skipped when undefined. */
|
|
11
11
|
migrationsFolder?: string;
|
|
12
|
+
/** Timeouts for the app's role; DEFAULT_ROLE_TIMEOUTS where not given. */
|
|
13
|
+
timeouts?: Partial<RoleTimeouts>;
|
|
12
14
|
}
|
|
15
|
+
/** Postgres durations (`30s`, `500ms`, `2min`) for the app's role; `0` turns one off. */
|
|
16
|
+
export interface RoleTimeouts {
|
|
17
|
+
/** Longest a single statement may run. */
|
|
18
|
+
statement: string;
|
|
19
|
+
/** Longest a transaction may sit idle, holding its locks and its pooled connection. */
|
|
20
|
+
idleInTransaction: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const DEFAULT_ROLE_TIMEOUTS: RoleTimeouts;
|
|
23
|
+
/** A Postgres duration as the env gives it, e.g. `WERVT_STATEMENT_TIMEOUT`; the fallback when empty. */
|
|
24
|
+
export declare function parseDuration(value: string | undefined, fallback: string, name?: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Sets the timeouts on an existing app role, for apps that aren't redeployed. New sessions get
|
|
27
|
+
* them; open ones keep theirs. Returns false when the app has no role.
|
|
28
|
+
*/
|
|
29
|
+
export declare function setRoleTimeouts({ app, adminUrl, timeouts }: Pick<ProvisionOptions, 'app' | 'adminUrl' | 'timeouts'>): Promise<boolean>;
|
|
13
30
|
export declare function dbRole(app: string): string;
|
|
14
31
|
export declare function hmac(secret: string, message: string): Promise<string>;
|
|
15
32
|
/** The app's Garage bucket and S3 key, derived from WERVT_DB_SECRET like role passwords. */
|
|
@@ -47,4 +64,4 @@ export declare function setupPooler({ adminUrl, password }: {
|
|
|
47
64
|
/** Drops the app's schema (all its data) and role. */
|
|
48
65
|
export declare function dropDatabase({ app, adminUrl }: Pick<ProvisionOptions, 'app' | 'adminUrl'>): Promise<void>;
|
|
49
66
|
//#endregion
|
|
50
|
-
export { StorageError, type StorageOptions, type StorageUsage, createStorage };
|
|
67
|
+
export { MAX_KEY_BYTES, type ObjectDetails, type ObjectListing, StorageError, type StorageOptions, type StorageQuota, type StorageUsage, type StoredObject, createStorage };
|
package/dist/provision.js
CHANGED
|
@@ -1,8 +1,53 @@
|
|
|
1
|
-
import { StorageError, createStorage } from "./storage.js";
|
|
1
|
+
import { MAX_KEY_BYTES, StorageError, createStorage } from "./storage.js";
|
|
2
2
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
3
3
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
4
4
|
import postgres from "postgres";
|
|
5
5
|
//#region src/provision.ts
|
|
6
|
+
const DEFAULT_ROLE_TIMEOUTS = {
|
|
7
|
+
statement: "30s",
|
|
8
|
+
idleInTransaction: "60s"
|
|
9
|
+
};
|
|
10
|
+
const DURATION_RE = /^\d+(?:ms|s|min|h)?$/;
|
|
11
|
+
/** A Postgres duration as the env gives it, e.g. `WERVT_STATEMENT_TIMEOUT`; the fallback when empty. */
|
|
12
|
+
function parseDuration(value, fallback, name = "timeout") {
|
|
13
|
+
const trimmed = value?.trim();
|
|
14
|
+
if (!trimmed) return fallback;
|
|
15
|
+
if (!DURATION_RE.test(trimmed)) throw new Error(`${name} must be a duration like 30s, 500ms or 2min (0 for none)`);
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
function roleTimeouts(timeouts) {
|
|
19
|
+
const merged = {
|
|
20
|
+
...DEFAULT_ROLE_TIMEOUTS,
|
|
21
|
+
...timeouts
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
statement: parseDuration(merged.statement, DEFAULT_ROLE_TIMEOUTS.statement),
|
|
25
|
+
idleInTransaction: parseDuration(merged.idleInTransaction, DEFAULT_ROLE_TIMEOUTS.idleInTransaction)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function applyRoleTimeouts(admin, role, timeouts) {
|
|
29
|
+
await admin.unsafe(`alter role "${role}" set statement_timeout = '${timeouts.statement}'`);
|
|
30
|
+
await admin.unsafe(`alter role "${role}" set idle_in_transaction_session_timeout = '${timeouts.idleInTransaction}'`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sets the timeouts on an existing app role, for apps that aren't redeployed. New sessions get
|
|
34
|
+
* them; open ones keep theirs. Returns false when the app has no role.
|
|
35
|
+
*/
|
|
36
|
+
async function setRoleTimeouts({ app, adminUrl, timeouts }) {
|
|
37
|
+
const role = dbRole(app);
|
|
38
|
+
const admin = postgres(adminUrl, {
|
|
39
|
+
max: 1,
|
|
40
|
+
onnotice: () => {}
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
const [row] = await admin`select exists(select 1 from pg_roles where rolname = ${role})`;
|
|
44
|
+
if (!row?.exists) return false;
|
|
45
|
+
await applyRoleTimeouts(admin, role, roleTimeouts(timeouts));
|
|
46
|
+
return true;
|
|
47
|
+
} finally {
|
|
48
|
+
await admin.end();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
6
51
|
function dbRole(app) {
|
|
7
52
|
return `app_${app.replaceAll("-", "_")}`;
|
|
8
53
|
}
|
|
@@ -56,6 +101,7 @@ async function provisionDatabase(options) {
|
|
|
56
101
|
await admin.unsafe(`create schema if not exists "${role}" authorization "${role}"`);
|
|
57
102
|
await claimSchema(admin, role);
|
|
58
103
|
await admin.unsafe(`alter role "${role}" set search_path = "${role}"`);
|
|
104
|
+
await applyRoleTimeouts(admin, role, roleTimeouts(options.timeouts));
|
|
59
105
|
for (const db of [
|
|
60
106
|
database,
|
|
61
107
|
"postgres",
|
|
@@ -78,6 +124,7 @@ async function provisionDatabase(options) {
|
|
|
78
124
|
onnotice: () => {}
|
|
79
125
|
});
|
|
80
126
|
try {
|
|
127
|
+
await client.unsafe("set statement_timeout = 0; set idle_in_transaction_session_timeout = 0");
|
|
81
128
|
await migrate(drizzle({ client }), {
|
|
82
129
|
migrationsFolder,
|
|
83
130
|
migrationsSchema: role
|
|
@@ -174,4 +221,4 @@ async function dropDatabase({ app, adminUrl }) {
|
|
|
174
221
|
}
|
|
175
222
|
}
|
|
176
223
|
//#endregion
|
|
177
|
-
export { POOLER_ROLE, StorageError, appDatabaseUrl, createStorage, dbPassword, dbRole, dropDatabase, hmac, provisionDatabase, setupPooler, storageCredentials };
|
|
224
|
+
export { DEFAULT_ROLE_TIMEOUTS, MAX_KEY_BYTES, POOLER_ROLE, StorageError, appDatabaseUrl, createStorage, dbPassword, dbRole, dropDatabase, hmac, parseDuration, provisionDatabase, setRoleTimeouts, setupPooler, storageCredentials };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Message, Offset, Row } from "@electric-sql/client";
|
|
2
|
+
//#region src/runtime/app/snapshot.d.ts
|
|
3
|
+
export interface ShapeSnapshot<T> {
|
|
4
|
+
rows: [string, T][];
|
|
5
|
+
offset: Offset;
|
|
6
|
+
handle?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The route answered 401: the shape needs a signed-in user and there is none, e.g. an anonymous
|
|
9
|
+
* visitor of a public app, or the deploy's page check. No rows, and nothing to stream.
|
|
10
|
+
*/
|
|
11
|
+
unauthorized?: true;
|
|
12
|
+
}
|
|
13
|
+
/** The rows of the shape at `url` (absolute) as of now, without subscribing. */
|
|
14
|
+
export declare function loadSnapshot<T extends Row<unknown>>(url: string, fetchClient?: typeof fetch): Promise<ShapeSnapshot<T>>;
|
|
15
|
+
/** Applies Electric change messages to `rows`. Returns whether anything changed. */
|
|
16
|
+
export declare function applyMessages<T extends Row<unknown>>(rows: Map<string, T>, messages: Message<T>[]): boolean;
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { shapeParser } from "../shared/shape.js";
|
|
2
|
+
import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
|
|
3
|
+
//#region src/runtime/app/snapshot.ts
|
|
4
|
+
/** The rows of the shape at `url` (absolute) as of now, without subscribing. */
|
|
5
|
+
async function loadSnapshot(url, fetchClient) {
|
|
6
|
+
const rows = /* @__PURE__ */ new Map();
|
|
7
|
+
let fail = () => {};
|
|
8
|
+
const stream = new ShapeStream({
|
|
9
|
+
url,
|
|
10
|
+
subscribe: false,
|
|
11
|
+
fetchClient,
|
|
12
|
+
parser: shapeParser,
|
|
13
|
+
onError: (e) => fail(e)
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
await new Promise((resolve, reject) => {
|
|
17
|
+
fail = reject;
|
|
18
|
+
stream.subscribe((messages) => {
|
|
19
|
+
applyMessages(rows, messages);
|
|
20
|
+
if (stream.isUpToDate) resolve();
|
|
21
|
+
}, (e) => reject(e));
|
|
22
|
+
});
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (e?.status === 401) return {
|
|
25
|
+
rows: [],
|
|
26
|
+
offset: "-1",
|
|
27
|
+
unauthorized: true
|
|
28
|
+
};
|
|
29
|
+
throw e;
|
|
30
|
+
} finally {
|
|
31
|
+
stream.unsubscribeAll();
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
rows: [...rows],
|
|
35
|
+
offset: stream.lastOffset,
|
|
36
|
+
handle: stream.shapeHandle
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Applies Electric change messages to `rows`. Returns whether anything changed. */
|
|
40
|
+
function applyMessages(rows, messages) {
|
|
41
|
+
let changed = false;
|
|
42
|
+
for (const message of messages) if (isChangeMessage(message)) {
|
|
43
|
+
const { key, value } = message;
|
|
44
|
+
switch (message.headers.operation) {
|
|
45
|
+
case "insert":
|
|
46
|
+
rows.set(key, value);
|
|
47
|
+
break;
|
|
48
|
+
case "update":
|
|
49
|
+
rows.set(key, {
|
|
50
|
+
...rows.get(key),
|
|
51
|
+
...value
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
case "delete": rows.delete(key);
|
|
55
|
+
}
|
|
56
|
+
changed = true;
|
|
57
|
+
} else if (isControlMessage(message) && message.headers.control === "must-refetch") {
|
|
58
|
+
rows.clear();
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
return changed;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { applyMessages, loadSnapshot };
|
|
@@ -18,6 +18,12 @@ interface ShapeRefs<T> {
|
|
|
18
18
|
data: ShallowRef<T[]>;
|
|
19
19
|
error: Ref<unknown>;
|
|
20
20
|
isLive: Ref<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* The route refused the snapshot for lack of a signed-in user (401): `data` is empty because
|
|
23
|
+
* nobody may see it, not because there are no rows. Lets public apps show "sign in" instead
|
|
24
|
+
* of an empty state.
|
|
25
|
+
*/
|
|
26
|
+
unauthorized: Ref<boolean>;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Live rows of an Electric shape served by one of the app's shape routes
|
|
@@ -44,4 +50,5 @@ export declare function preloadShape<P extends keyof WervtShapeRoutes>(path: P,
|
|
|
44
50
|
export declare function preloadShape(path: string, options?: {
|
|
45
51
|
key?: string;
|
|
46
52
|
}): Promise<void>;
|
|
47
|
-
//#endregion
|
|
53
|
+
//#endregion
|
|
54
|
+
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { shapeParser } from "../shared/shape.js";
|
|
2
|
-
import {
|
|
2
|
+
import { applyMessages, loadSnapshot } from "./snapshot.js";
|
|
3
|
+
import { ShapeStream } from "@electric-sql/client";
|
|
3
4
|
import { getCurrentScope, onScopeDispose, ref, shallowRef } from "vue";
|
|
4
5
|
import { onNuxtReady, useAsyncData, useRequestEvent, useRequestFetch } from "#app";
|
|
5
6
|
//#region src/runtime/app/useShape.ts
|
|
@@ -8,13 +9,14 @@ function useShape(path, options = {}) {
|
|
|
8
9
|
const fetchClient = import.meta.server ? inProcessFetch() : void 0;
|
|
9
10
|
const asyncData = useAsyncData(key, () => {
|
|
10
11
|
const live = import.meta.client ? shapes.get(key) : void 0;
|
|
11
|
-
return live?.stream ? live.settled.then(() => snapshotOf(live)) : loadSnapshot(path, fetchClient);
|
|
12
|
+
return live?.stream ? live.settled.then(() => snapshotOf(live)) : loadSnapshot(absolute(path), fetchClient);
|
|
12
13
|
});
|
|
13
14
|
const shape = import.meta.client && options.live !== false ? acquire(key, path) : (import.meta.client ? shapes.get(key) : void 0) ?? createShape(path);
|
|
14
15
|
const result = {
|
|
15
16
|
data: shape.data,
|
|
16
17
|
error: shape.error,
|
|
17
|
-
isLive: shape.isLive
|
|
18
|
+
isLive: shape.isLive,
|
|
19
|
+
unauthorized: shape.unauthorized
|
|
18
20
|
};
|
|
19
21
|
const ready = Promise.resolve(asyncData).then(() => {
|
|
20
22
|
if (!shape.stream && !shape.seeded) {
|
|
@@ -22,6 +24,7 @@ function useShape(path, options = {}) {
|
|
|
22
24
|
shape.data.value = [...shape.rows.values()];
|
|
23
25
|
shape.offset = asyncData.data.value?.offset;
|
|
24
26
|
shape.handle = asyncData.data.value?.handle;
|
|
27
|
+
shape.unauthorized.value = asyncData.data.value?.unauthorized ?? false;
|
|
25
28
|
shape.seeded = true;
|
|
26
29
|
}
|
|
27
30
|
const error = asyncData.error.value ?? null;
|
|
@@ -34,7 +37,7 @@ function useShape(path, options = {}) {
|
|
|
34
37
|
if (import.meta.client && options.live !== false) {
|
|
35
38
|
onNuxtReady(async () => {
|
|
36
39
|
await ready;
|
|
37
|
-
start(shape);
|
|
40
|
+
if (!shape.unauthorized.value) start(shape);
|
|
38
41
|
});
|
|
39
42
|
if (getCurrentScope()) onScopeDispose(() => release(key, shape, KEEP_AFTER_UNMOUNT_MS));
|
|
40
43
|
}
|
|
@@ -63,6 +66,7 @@ function createShape(path) {
|
|
|
63
66
|
error: ref(null),
|
|
64
67
|
isLive: ref(false),
|
|
65
68
|
seeded: false,
|
|
69
|
+
unauthorized: ref(false),
|
|
66
70
|
settled,
|
|
67
71
|
settle,
|
|
68
72
|
users: 0
|
|
@@ -118,54 +122,6 @@ function snapshotOf(shape) {
|
|
|
118
122
|
handle: shape.stream?.shapeHandle ?? shape.handle
|
|
119
123
|
};
|
|
120
124
|
}
|
|
121
|
-
async function loadSnapshot(path, fetchClient) {
|
|
122
|
-
const rows = /* @__PURE__ */ new Map();
|
|
123
|
-
let fail = () => {};
|
|
124
|
-
const stream = new ShapeStream({
|
|
125
|
-
url: absolute(path),
|
|
126
|
-
subscribe: false,
|
|
127
|
-
fetchClient,
|
|
128
|
-
parser: shapeParser,
|
|
129
|
-
onError: (e) => fail(e)
|
|
130
|
-
});
|
|
131
|
-
await new Promise((resolve, reject) => {
|
|
132
|
-
fail = reject;
|
|
133
|
-
stream.subscribe((messages) => {
|
|
134
|
-
applyMessages(rows, messages);
|
|
135
|
-
if (stream.isUpToDate) resolve();
|
|
136
|
-
}, (e) => reject(e));
|
|
137
|
-
});
|
|
138
|
-
stream.unsubscribeAll();
|
|
139
|
-
return {
|
|
140
|
-
rows: [...rows],
|
|
141
|
-
offset: stream.lastOffset,
|
|
142
|
-
handle: stream.shapeHandle
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
/** Applies Electric change messages to `rows`. Returns whether anything changed. */
|
|
146
|
-
function applyMessages(rows, messages) {
|
|
147
|
-
let changed = false;
|
|
148
|
-
for (const message of messages) if (isChangeMessage(message)) {
|
|
149
|
-
const { key, value } = message;
|
|
150
|
-
switch (message.headers.operation) {
|
|
151
|
-
case "insert":
|
|
152
|
-
rows.set(key, value);
|
|
153
|
-
break;
|
|
154
|
-
case "update":
|
|
155
|
-
rows.set(key, {
|
|
156
|
-
...rows.get(key),
|
|
157
|
-
...value
|
|
158
|
-
});
|
|
159
|
-
break;
|
|
160
|
-
case "delete": rows.delete(key);
|
|
161
|
-
}
|
|
162
|
-
changed = true;
|
|
163
|
-
} else if (isControlMessage(message) && message.headers.control === "must-refetch") {
|
|
164
|
-
rows.clear();
|
|
165
|
-
changed = true;
|
|
166
|
-
}
|
|
167
|
-
return changed;
|
|
168
|
-
}
|
|
169
125
|
/**
|
|
170
126
|
* During SSR, calls the shape route in-process with the request's headers (cookies, auth).
|
|
171
127
|
* Nitro 2 (Nuxt 4.6) has `event.fetch`, which returns a Response; on Nitro 3 (Nuxt 5) the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme } from "./components.js";
|
|
2
|
+
import { InviteEmail } from "./invite.js";
|
|
2
3
|
import { EmailProps, RenderedEmail, renderEmail } from "./render.js";
|
|
3
|
-
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, type EmailProps, EmailText, type RenderedEmail, WervtEmail, emailTheme, renderEmail };
|
|
4
|
+
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, type EmailProps, EmailText, InviteEmail, type RenderedEmail, WervtEmail, emailTheme, renderEmail };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme } from "./components.js";
|
|
2
|
+
import { InviteEmail } from "./invite.js";
|
|
2
3
|
import { renderEmail } from "./render.js";
|
|
3
|
-
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, WervtEmail, emailTheme, renderEmail };
|
|
4
|
+
export { EmailButton, EmailCode, EmailDivider, EmailHeading, EmailLink, EmailMuted, EmailText, InviteEmail, WervtEmail, emailTheme, renderEmail };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/runtime/email/invite.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* An invitation to an app: who invited, where to go, and that signing in with this address is
|
|
4
|
+
* all it takes. Send it with `sendMail({ to, template: InviteEmail, props: { by, app, url } })`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const InviteEmail: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
7
|
+
/** Who invited them, by name or email. */
|
|
8
|
+
by: {
|
|
9
|
+
type: StringConstructor;
|
|
10
|
+
required: true;
|
|
11
|
+
};
|
|
12
|
+
/** The app they're invited to, as people know it. */
|
|
13
|
+
app: {
|
|
14
|
+
type: StringConstructor;
|
|
15
|
+
required: true;
|
|
16
|
+
};
|
|
17
|
+
/** Where the button goes: the app, or a link that lets them in. */
|
|
18
|
+
url: {
|
|
19
|
+
type: StringConstructor;
|
|
20
|
+
required: true;
|
|
21
|
+
};
|
|
22
|
+
/** What the app is, in a sentence after "<by> invited you to <app>." */
|
|
23
|
+
message: {
|
|
24
|
+
type: StringConstructor;
|
|
25
|
+
default: string;
|
|
26
|
+
};
|
|
27
|
+
/** The button's label; "Open <app>" unless set. */
|
|
28
|
+
action: {
|
|
29
|
+
type: StringConstructor;
|
|
30
|
+
default: string;
|
|
31
|
+
};
|
|
32
|
+
/** A hint under the button; by default, to sign in with this address. */
|
|
33
|
+
note: {
|
|
34
|
+
type: StringConstructor;
|
|
35
|
+
default: string;
|
|
36
|
+
};
|
|
37
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
38
|
+
[key: string]: any;
|
|
39
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
40
|
+
/** Who invited them, by name or email. */
|
|
41
|
+
by: {
|
|
42
|
+
type: StringConstructor;
|
|
43
|
+
required: true;
|
|
44
|
+
};
|
|
45
|
+
/** The app they're invited to, as people know it. */
|
|
46
|
+
app: {
|
|
47
|
+
type: StringConstructor;
|
|
48
|
+
required: true;
|
|
49
|
+
};
|
|
50
|
+
/** Where the button goes: the app, or a link that lets them in. */
|
|
51
|
+
url: {
|
|
52
|
+
type: StringConstructor;
|
|
53
|
+
required: true;
|
|
54
|
+
};
|
|
55
|
+
/** What the app is, in a sentence after "<by> invited you to <app>." */
|
|
56
|
+
message: {
|
|
57
|
+
type: StringConstructor;
|
|
58
|
+
default: string;
|
|
59
|
+
};
|
|
60
|
+
/** The button's label; "Open <app>" unless set. */
|
|
61
|
+
action: {
|
|
62
|
+
type: StringConstructor;
|
|
63
|
+
default: string;
|
|
64
|
+
};
|
|
65
|
+
/** A hint under the button; by default, to sign in with this address. */
|
|
66
|
+
note: {
|
|
67
|
+
type: StringConstructor;
|
|
68
|
+
default: string;
|
|
69
|
+
};
|
|
70
|
+
}>> & Readonly<{}>, {
|
|
71
|
+
message: string;
|
|
72
|
+
action: string;
|
|
73
|
+
note: string;
|
|
74
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
75
|
+
//#endregion
|