- {{ user === 'anon' ? '' : `Logged in as ${user}` }}
+ {{ user ? `Logged in as ${user}` : '' }}
diff --git a/test/fixtures/nuxt/app/composables/zero.ts b/test/fixtures/nuxt/app/composables/zero.ts
index 2d974a2..83df64f 100644
--- a/test/fixtures/nuxt/app/composables/zero.ts
+++ b/test/fixtures/nuxt/app/composables/zero.ts
@@ -1,121 +1,21 @@
-import {
- defineMutator,
- defineMutators,
- defineQueries,
- defineQuery,
- escapeLike,
-} from '@rocicorp/zero'
import { decodeJwt } from 'jose'
import { createZeroComposables } from 'zero-vue'
-import z from 'zod'
-import { schema, zql } from '#fx/db/schema'
-
-export interface ZeroContext {
- userID: string
-}
-
-declare module '@rocicorp/zero' {
- interface DefaultTypes {
- context: ZeroContext
- }
-}
-
-export const mutators = defineMutators({
- message: {
- insert: defineMutator(
- z.object({
- mediumID: z.string(),
- body: z.string(),
- id: z.string(),
- timestamp: z.number(),
- }),
- async ({ tx, ctx: { userID }, args: { mediumID, body, id, timestamp } }) => {
- return tx.mutate.message.insert({
- senderID: userID,
- mediumID,
- body,
- id,
- timestamp,
- })
- },
- ),
- update: defineMutator(
- z.object({ id: z.string(), body: z.string() }),
- async ({ tx, ctx: { userID }, args: { id, body } }) => {
- const messageToEdit = await tx.run(
- zql.message.where('id', id).one(),
- )
- if (!messageToEdit) {
- throw new Error(`Message with id ${id} not found`)
- }
-
- if (messageToEdit.senderID !== userID) {
- throw new Error(`You aren't allowed to edit this message`)
- }
-
- return tx.mutate.message.update({ id, body })
- },
- ),
- delete: defineMutator(
- z.object({ id: z.string() }),
- async ({ tx, ctx: { userID }, args: { id } }) => {
- if (!userID) {
- throw new Error('You must be logged in to delete')
- }
-
- return tx.mutate.message.delete({ id })
- },
- ),
- },
-})
-
-export const queries = defineQueries({
- messages: {
- all: defineQuery(() => zql.message),
- filtered: defineQuery(
- z.object({
- filterUser: z.string().optional(),
- filterText: z.string().optional(),
- }),
- ({ args: { filterUser, filterText } }) => {
- let filtered = zql.message
- .related('medium', medium => medium.one())
- .related('sender', sender => sender.one())
- .orderBy('timestamp', 'desc')
-
- if (filterUser) {
- filtered = filtered.where('senderID', filterUser)
- }
-
- if (filterText) {
- filtered = filtered.where('body', 'LIKE', `%${escapeLike(filterText)}%`)
- }
-
- return filtered
- },
- ),
- },
- users: {
- all: defineQuery(() => zql.user),
- },
- mediums: {
- all: defineQuery(() => zql.medium),
- },
-})
+import { mutators, schema } from '#fx/db/schema'
function createComposables() {
return createZeroComposables(() => {
const jwt = useCookie('jwt')
const decoded = jwt.value ? decodeJwt(jwt.value) : undefined
- const userID = typeof decoded?.sub === 'string' ? decoded.sub : 'anon'
+ const userID = typeof decoded?.sub === 'string' ? decoded.sub : undefined
const config = useRuntimeConfig()
return {
userID,
- auth: jwt.value || undefined,
context: { userID },
- server: import.meta.client ? config.public.zero.cacheURL : undefined,
+ cacheURL: import.meta.client ? config.public.zero.cacheURL : undefined,
+ queryURL: config.public.zero.queryURL,
+ mutateURL: config.public.zero.mutateURL,
schema,
mutators,
kvStore: 'mem' as const,
diff --git a/test/fixtures/nuxt/nuxt.config.ts b/test/fixtures/nuxt/nuxt.config.ts
index 49732ad..2cacd24 100644
--- a/test/fixtures/nuxt/nuxt.config.ts
+++ b/test/fixtures/nuxt/nuxt.config.ts
@@ -13,12 +13,12 @@ export default defineNuxtConfig({
},
css: ['~/assets/index.css'],
runtimeConfig: {
- zero: {
- authSecret: '',
- },
+ authSecret: '',
public: {
zero: {
cacheURL: '',
+ queryURL: '',
+ mutateURL: '',
},
},
},
@@ -28,7 +28,12 @@ export default defineNuxtConfig({
esbuildOptions: {
target: 'es2022',
},
- include: ['@rocicorp/zero'],
+ include: [
+ '@rocicorp/zero',
+ '@rocicorp/zero/bindings',
+ 'jose',
+ 'zod',
+ ],
},
},
})
diff --git a/test/fixtures/nuxt/package.json b/test/fixtures/nuxt/package.json
index 6a56e8b..22aad1c 100644
--- a/test/fixtures/nuxt/package.json
+++ b/test/fixtures/nuxt/package.json
@@ -9,7 +9,7 @@
"dev:zero-cache": "cd ../_shared && zero-cache-dev -p db/schema.ts",
"dev:db-up": "cd ../_shared && docker compose --env-file .env -f docker/docker-compose.yml up",
"dev:db-down": "cd ../_shared && docker compose --env-file .env -f docker/docker-compose.yml down",
- "dev:clean": "cd ../_shared && source .env && docker volume rm -f docker_zstart_pgdata && rm -rf \"${ZERO_REPLICA_FILE}\"*",
+ "dev:clean": "cd ../_shared && source .env && docker volume rm -f zero-vue_zstart_pgdata && rm -rf \"${ZERO_REPLICA_FILE}\"*",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
@@ -18,7 +18,8 @@
},
"dependencies": {
"jose": "^6.2.3",
- "nuxt": "^4.2.1",
+ "nuxt": "^4.4.8",
+ "postgres": "3.4.7",
"vue": "^3.5.34",
"zero-vue": "link:",
"zod": "4.4.3"
@@ -26,9 +27,9 @@
"devDependencies": {
"@nuxt/test-utils": "^4.0.3",
"@types/node": "24.11.0",
- "happy-dom": "^20.0.0",
+ "happy-dom": "^20.10.6",
"typescript": "6.0.3",
"vitest": "^4.1.7",
- "vue-tsc": "3.2.8"
+ "vue-tsc": "3.3.6"
}
}
diff --git a/test/fixtures/nuxt/server/api/login.get.ts b/test/fixtures/nuxt/server/api/login.get.ts
index 7ad2592..c711b89 100644
--- a/test/fixtures/nuxt/server/api/login.get.ts
+++ b/test/fixtures/nuxt/server/api/login.get.ts
@@ -1,36 +1,7 @@
-import { SignJWT } from 'jose'
-
-// See docker/seed.sql for the seeded user list.
-const userIDs = [
- '6z7dkeVLNm',
- 'ycD76wW4R2',
- 'IoQSaxeVO5',
- 'WndZWmGkO4',
- 'ENzoNm7g4E',
- 'dLKecN3ntd',
- '7VoEoJWEwn',
- 'enVvyDlBul',
- '9ogaDuDNFx',
-]
-
-function randomInt(max: number) {
- return Math.floor(Math.random() * max)
-}
+import { createJWT } from '../utils/auth'
export default defineEventHandler(async (event) => {
- const config = useRuntimeConfig(event)
- if (!config.zero.authSecret) {
- throw createError({ statusCode: 500, statusMessage: 'ZERO_AUTH_SECRET is not configured' })
- }
-
- const jwt = await new SignJWT({
- sub: userIDs[randomInt(userIDs.length)],
- iat: Math.floor(Date.now() / 1000),
- })
- .setProtectedHeader({ alg: 'HS256' })
- .setExpirationTime('30days')
- .sign(new TextEncoder().encode(config.zero.authSecret))
-
+ const jwt = await createJWT(event)
setCookie(event, 'jwt', jwt, {
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
})
diff --git a/test/fixtures/nuxt/server/api/zero/mutate.post.ts b/test/fixtures/nuxt/server/api/zero/mutate.post.ts
new file mode 100644
index 0000000..8273be1
--- /dev/null
+++ b/test/fixtures/nuxt/server/api/zero/mutate.post.ts
@@ -0,0 +1,34 @@
+import process from 'node:process'
+import { mustGetMutator } from '@rocicorp/zero'
+import { handleMutateRequest } from '@rocicorp/zero/server'
+import { zeroPostgresJS } from '@rocicorp/zero/server/adapters/postgresjs'
+import { toWebRequest } from 'h3'
+import postgres from 'postgres'
+
+import { mutators, schema } from '#fx/db/schema'
+import { getUserID } from '../../utils/auth'
+
+function getUpstreamDB() {
+ const upstreamDB = process.env.ZERO_UPSTREAM_DB
+ if (!upstreamDB) {
+ throw new Error('ZERO_UPSTREAM_DB is not configured')
+ }
+ return upstreamDB
+}
+
+const dbProvider = zeroPostgresJS(schema, postgres(getUpstreamDB()))
+
+export default defineEventHandler(async (event) => {
+ const userID = await getUserID(event)
+ const ctx = { userID }
+
+ return handleMutateRequest({
+ dbProvider,
+ handler: transact => transact((tx, name, args) => {
+ const mutator = mustGetMutator(mutators, name)
+ return mutator.fn({ tx, args, ctx })
+ }),
+ request: toWebRequest(event),
+ userID,
+ })
+})
diff --git a/test/fixtures/nuxt/server/api/zero/query.post.ts b/test/fixtures/nuxt/server/api/zero/query.post.ts
new file mode 100644
index 0000000..b1fdb5a
--- /dev/null
+++ b/test/fixtures/nuxt/server/api/zero/query.post.ts
@@ -0,0 +1,21 @@
+import { mustGetQuery } from '@rocicorp/zero'
+import { handleQueryRequest } from '@rocicorp/zero/server'
+import { toWebRequest } from 'h3'
+
+import { queries, schema } from '#fx/db/schema'
+import { getUserID } from '../../utils/auth'
+
+export default defineEventHandler(async (event) => {
+ const userID = await getUserID(event)
+ const ctx = { userID }
+
+ return handleQueryRequest({
+ handler: (name, args) => {
+ const query = mustGetQuery(queries, name)
+ return query.fn({ args, ctx })
+ },
+ schema,
+ request: toWebRequest(event),
+ userID,
+ })
+})
diff --git a/test/fixtures/nuxt/server/utils/auth.ts b/test/fixtures/nuxt/server/utils/auth.ts
new file mode 100644
index 0000000..1161ade
--- /dev/null
+++ b/test/fixtures/nuxt/server/utils/auth.ts
@@ -0,0 +1,46 @@
+import type { H3Event } from 'h3'
+import { randomInt } from 'node:crypto'
+import { createError, getCookie } from 'h3'
+import { jwtVerify, SignJWT } from 'jose'
+
+// See docker/seed.sql for the seeded user list.
+const userIDs = [
+ '6z7dkeVLNm',
+ 'ycD76wW4R2',
+ 'IoQSaxeVO5',
+ 'WndZWmGkO4',
+ 'ENzoNm7g4E',
+ 'dLKecN3ntd',
+ '7VoEoJWEwn',
+ 'enVvyDlBul',
+ '9ogaDuDNFx',
+]
+
+function getAuthSecret(event: H3Event) {
+ const config = useRuntimeConfig(event)
+ if (typeof config.authSecret !== 'string' || !config.authSecret) {
+ throw createError({ statusCode: 500, statusMessage: 'NUXT_AUTH_SECRET is not configured' })
+ }
+
+ return new TextEncoder().encode(config.authSecret)
+}
+
+export async function createJWT(event: H3Event) {
+ return await new SignJWT({
+ sub: userIDs[randomInt(userIDs.length)]!,
+ iat: Math.floor(Date.now() / 1000),
+ })
+ .setProtectedHeader({ alg: 'HS256' })
+ .setExpirationTime('30days')
+ .sign(getAuthSecret(event))
+}
+
+export async function getUserID(event: H3Event) {
+ const jwt = getCookie(event, 'jwt')
+ if (!jwt) {
+ return undefined
+ }
+
+ const { payload } = await jwtVerify(jwt, getAuthSecret(event))
+ return typeof payload.sub === 'string' ? payload.sub : undefined
+}
diff --git a/test/fixtures/nuxt/test/e2e.test.ts b/test/fixtures/nuxt/test/e2e.test.ts
index 4ee1b81..aebb294 100644
--- a/test/fixtures/nuxt/test/e2e.test.ts
+++ b/test/fixtures/nuxt/test/e2e.test.ts
@@ -8,7 +8,7 @@ const authSecret = 'test-secret-for-e2e'
await setup({
rootDir: fileURLToPath(new URL('..', import.meta.url)),
env: {
- NUXT_ZERO_AUTH_SECRET: authSecret,
+ NUXT_AUTH_SECRET: authSecret,
},
})
diff --git a/test/fixtures/vue/.env b/test/fixtures/vue/.env
index 494e6c1..ed4149f 100644
--- a/test/fixtures/vue/.env
+++ b/test/fixtures/vue/.env
@@ -1,2 +1,5 @@
-# VITE_PUBLIC_AUTH_SECRET='secretkey'
-# VITE_PUBLIC_ZERO_CACHE_URL='http://localhost:4848'
+VITE_PUBLIC_ZERO_CACHE_URL="http://localhost:4848"
+VITE_PUBLIC_ZERO_QUERY_URL="http://localhost:3000/api/zero/query"
+VITE_PUBLIC_ZERO_MUTATE_URL="http://localhost:3000/api/zero/mutate"
+VITE_AUTH_SECRET="authSecret"
+ZERO_UPSTREAM_DB="postgresql://user:password@127.0.0.1:5430/zstart"
diff --git a/test/fixtures/vue/package.json b/test/fixtures/vue/package.json
index a412fc9..16ea0c3 100644
--- a/test/fixtures/vue/package.json
+++ b/test/fixtures/vue/package.json
@@ -10,12 +10,14 @@
"dev:zero-cache": "cd ../_shared && zero-cache-dev -p db/schema.ts",
"dev:db-up": "cd ../_shared && docker compose --env-file .env -f docker/docker-compose.yml up",
"dev:db-down": "cd ../_shared && docker compose --env-file .env -f docker/docker-compose.yml down",
- "dev:clean": "cd ../_shared && source .env && docker volume rm -f docker_zstart_pgdata && rm -rf \"${ZERO_REPLICA_FILE}\"*",
+ "dev:clean": "cd ../_shared && source .env && docker volume rm -f zero-vue_zstart_pgdata && rm -rf \"${ZERO_REPLICA_FILE}\"*",
"test:types": "vue-tsc --build"
},
"dependencies": {
"@vueuse/integrations": "^14.3.0",
+ "h3": "^1.15.11",
"jose": "^6.2.3",
+ "postgres": "3.4.7",
"universal-cookie": "^8.1.2",
"vue": "^3.5.34",
"zero-vue": "link:",
@@ -28,7 +30,7 @@
"@vitejs/plugin-vue": "6.0.7",
"@vue/tsconfig": "0.9.1",
"typescript": "6.0.3",
- "vite": "8.0.14",
- "vue-tsc": "3.2.8"
+ "vite": "8.1.3",
+ "vue-tsc": "3.3.6"
}
}
diff --git a/test/fixtures/vue/server/auth.ts b/test/fixtures/vue/server/auth.ts
new file mode 100644
index 0000000..b278658
--- /dev/null
+++ b/test/fixtures/vue/server/auth.ts
@@ -0,0 +1,51 @@
+import { randomInt } from 'node:crypto'
+import process from 'node:process'
+import { createError } from 'h3'
+import { jwtVerify, SignJWT } from 'jose'
+
+const userIDs = [
+ '6z7dkeVLNm',
+ 'ycD76wW4R2',
+ 'IoQSaxeVO5',
+ 'WndZWmGkO4',
+ 'ENzoNm7g4E',
+ 'dLKecN3ntd',
+ '7VoEoJWEwn',
+ 'enVvyDlBul',
+ '9ogaDuDNFx',
+]
+
+function getAuthSecret() {
+ const authSecret = process.env.VITE_AUTH_SECRET
+ if (!authSecret) {
+ throw createError({ statusCode: 500, statusMessage: 'VITE_AUTH_SECRET is not configured' })
+ }
+
+ return new TextEncoder().encode(authSecret)
+}
+
+export async function createJWT() {
+ return await new SignJWT({
+ sub: userIDs[randomInt(userIDs.length)]!,
+ iat: Math.floor(Date.now() / 1000),
+ })
+ .setProtectedHeader({ alg: 'HS256' })
+ .setExpirationTime('30days')
+ .sign(getAuthSecret())
+}
+
+export async function getUserID(request: Request) {
+ const cookie = request.headers
+ .get('cookie')
+ ?.split(';')
+ .map(cookie => cookie.trim())
+ .find(cookie => cookie.startsWith('jwt='))
+
+ if (!cookie) {
+ return undefined
+ }
+
+ const jwt = cookie.slice('jwt='.length)
+ const { payload } = await jwtVerify(jwt, getAuthSecret())
+ return typeof payload.sub === 'string' ? payload.sub : undefined
+}
diff --git a/test/fixtures/vue/server/index.ts b/test/fixtures/vue/server/index.ts
new file mode 100644
index 0000000..eae56a5
--- /dev/null
+++ b/test/fixtures/vue/server/index.ts
@@ -0,0 +1,19 @@
+import { createApp, createRouter, defineEventHandler, setCookie, toWebRequest } from 'h3'
+import { createJWT } from './auth'
+import { handleMutate } from './mutate'
+import { handleQuery } from './query'
+
+const router = createRouter()
+
+router.get('/api/login', defineEventHandler(async (event) => {
+ const jwt = await createJWT()
+ setCookie(event, 'jwt', jwt, {
+ expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
+ })
+ return 'ok'
+}))
+
+router.post('/api/zero/query', defineEventHandler(event => handleQuery(toWebRequest(event))))
+router.post('/api/zero/mutate', defineEventHandler(event => handleMutate(toWebRequest(event))))
+
+export const app = createApp().use(router)
diff --git a/test/fixtures/vue/server/mutate.ts b/test/fixtures/vue/server/mutate.ts
new file mode 100644
index 0000000..26da44b
--- /dev/null
+++ b/test/fixtures/vue/server/mutate.ts
@@ -0,0 +1,33 @@
+import process from 'node:process'
+import { mustGetMutator } from '@rocicorp/zero'
+import { handleMutateRequest } from '@rocicorp/zero/server'
+import { zeroPostgresJS } from '@rocicorp/zero/server/adapters/postgresjs'
+import postgres from 'postgres'
+
+import { mutators, schema } from '#fx/db/schema'
+import { getUserID } from './auth'
+
+function getUpstreamDB() {
+ const upstreamDB = process.env.ZERO_UPSTREAM_DB
+ if (!upstreamDB) {
+ throw new Error('ZERO_UPSTREAM_DB is not configured')
+ }
+ return upstreamDB
+}
+
+const dbProvider = zeroPostgresJS(schema, postgres(getUpstreamDB()))
+
+export async function handleMutate(request: Request) {
+ const userID = await getUserID(request)
+ const ctx = { userID }
+
+ return handleMutateRequest({
+ dbProvider,
+ handler: transact => transact((tx, name, args) => {
+ const mutator = mustGetMutator(mutators, name)
+ return mutator.fn({ tx, args, ctx })
+ }),
+ request,
+ userID,
+ })
+}
diff --git a/test/fixtures/vue/server/query.ts b/test/fixtures/vue/server/query.ts
new file mode 100644
index 0000000..5b78d0f
--- /dev/null
+++ b/test/fixtures/vue/server/query.ts
@@ -0,0 +1,20 @@
+import { mustGetQuery } from '@rocicorp/zero'
+import { handleQueryRequest } from '@rocicorp/zero/server'
+
+import { queries, schema } from '#fx/db/schema'
+import { getUserID } from './auth'
+
+export async function handleQuery(request: Request) {
+ const userID = await getUserID(request)
+ const ctx = { userID }
+
+ return handleQueryRequest({
+ handler: (name, args) => {
+ const query = mustGetQuery(queries, name)
+ return query.fn({ args, ctx })
+ },
+ schema,
+ request,
+ userID,
+ })
+}
diff --git a/test/fixtures/vue/src/app.vue b/test/fixtures/vue/src/app.vue
index 6b7a58f..bb500a9 100644
--- a/test/fixtures/vue/src/app.vue
+++ b/test/fixtures/vue/src/app.vue
@@ -1,7 +1,6 @@