Skip to content

Commit b42e6ee

Browse files
committed
chore: initial commit
0 parents  commit b42e6ee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+9710
-0
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DB_URL=postgresql://postgres:postgres@db.ztest.local:5432/ztest_dev
2+
VITE_SYNC_URL=https://sync.ztest.local

.github/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# ztest
2+
3+
Install the following tools:
4+
5+
- [direnv](https://github.com/direnv/direnv) - loads environment variables
6+
- [OrbStack](https://orbstack.dev) - runs dev containers, local HTTPS/domains, etc.
7+
8+
Run the following commands:
9+
10+
```bash
11+
# Clone and copy empty variables
12+
gh repo clone tmm/ztest
13+
cp .env.example .env
14+
cp .dev.vars.example .dev.vars
15+
16+
pnpm install # Install local dependencies
17+
pnpm dev -d # Start containers
18+
pnpm db:migrate latest # Setup database and run migrations
19+
pnpm db:codegen # Generate Kysely/Zero types from database
20+
pnpm gen:types # Generate Cloudflare types
21+
pnpm test # Test Zero queries/mutators
22+
```
23+
24+
## License
25+
26+
[MIT](/LICENSE)

.github/actions/pnpm/action.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: 'pnpm'
2+
3+
inputs:
4+
node-version:
5+
required: true
6+
working-directory:
7+
default: '.'
8+
required: false
9+
10+
runs:
11+
using: "composite"
12+
steps:
13+
- name: Set up pnpm
14+
uses: pnpm/action-setup@v4
15+
with:
16+
package_json_file: ${{ inputs.working-directory }}/package.json
17+
18+
- name: Setup Node.js with pnpm cache
19+
uses: actions/setup-node@v4
20+
with:
21+
cache: pnpm
22+
cache-dependency-path: ${{ inputs.working-directory }}/pnpm-lock.yaml
23+
node-version: ${{ inputs.node-version }}
24+
25+
- name: Install dependencies
26+
working-directory: ${{ inputs.working-directory }}
27+
run: pnpm install
28+
shell: bash
29+

.github/workflows/check.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Check
2+
on:
3+
workflow_call:
4+
workflow_dispatch:
5+
permissions:
6+
contents: read
7+
8+
jobs:
9+
app:
10+
name: app
11+
env:
12+
DB_URL: postgres://postgres:postgres@localhost:5432/postgres
13+
outputs:
14+
artifact_id: ${{ steps.upload-artifact.outputs.artifact-id }}
15+
runs-on: ubuntu-latest
16+
services:
17+
db:
18+
image: postgres:16.2-alpine
19+
env:
20+
POSTGRES_DB: postgres
21+
POSTGRES_PASSWORD: postgres
22+
options: >-
23+
--health-cmd pg_isready
24+
--health-interval 10s
25+
--health-retries 5
26+
--health-timeout 5s
27+
ports: ['5432:5432']
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@v4
31+
32+
- name: Install dependencies
33+
uses: ./.github/actions/pnpm
34+
with:
35+
node-version: 24.10.0
36+
37+
- name: Check code
38+
run: pnpm check
39+
40+
- name: Test migrations
41+
run: pnpm db:migrate latest
42+
43+
- name: Generate code
44+
run: |
45+
pnpm db:codegen
46+
cp .dev.vars.example .dev.vars
47+
pnpm gen:types
48+
rm .dev.vars
49+
50+
- name: Build
51+
run: pnpm vite build
52+
53+
- name: Check types
54+
run: pnpm check:types
55+
56+
- name: Test
57+
run: pnpm test

.github/workflows/production.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: Production
2+
on:
3+
push:
4+
branches: [main]
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
check:
11+
name: Check
12+
uses: ./.github/workflows/check.yml

.github/workflows/pull-request.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: Pull Request
2+
on:
3+
pull_request:
4+
types: [opened, reopened, synchronize, ready_for_review]
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
check:
11+
name: Check
12+
uses: ./.github/workflows/check.yml

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
*.local
2+
*.log
3+
.DS_Store
4+
.dev.vars*
5+
.env
6+
.env.json
7+
.pnpm-store
8+
.sonda
9+
.vscode/*
10+
.wrangler
11+
dist
12+
generated
13+
logs
14+
node-compile-cache
15+
node_modules
16+
pnpm-debug.log*
17+
sonda-report.html

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
auto-install-peers=false
2+
enable-pre-post-scripts=true

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2025-present weth, LLC
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

api/index.push.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {
2+
type CustomMutatorImpl as CustomMutatorFn,
3+
type Schema,
4+
withValidation,
5+
} from "@rocicorp/zero"
6+
import {
7+
PostgresJSConnection,
8+
PushProcessor,
9+
type ServerTransaction,
10+
ZQLDatabase,
11+
} from "@rocicorp/zero/pg"
12+
import { handleGetQueriesRequest } from "@rocicorp/zero/server"
13+
import postgres from "postgres"
14+
import * as z from "zod/mini"
15+
import { AuthData } from "../shared/auth.ts"
16+
import { jsonCodec } from "../shared/codec.ts"
17+
import { createMutators, type mutators } from "../shared/mutators.ts"
18+
import * as queries from "../shared/queries.ts"
19+
import { schema } from "../shared/schema.ts"
20+
import * as Cookie from "./lib/cookie.ts"
21+
import * as Urls from "./lib/urls.ts"
22+
23+
export default {
24+
async fetch(req, env) {
25+
const url = new URL(req.url)
26+
const headers = {
27+
"Access-Control-Allow-Credentials": "true",
28+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
29+
"Access-Control-Allow-Methods": "POST, OPTIONS",
30+
"Access-Control-Allow-Origin": Urls.origins(
31+
Urls.fromString(env.APP_URL),
32+
).join(","),
33+
"Access-Control-Max-Age": "86400",
34+
} satisfies HeadersInit
35+
36+
if (req.method === "OPTIONS")
37+
return new Response(null, { headers, status: 204 })
38+
39+
if (req.method === "GET" && url.pathname === "/ping")
40+
return new Response("pong", { headers, status: 200 })
41+
42+
if (
43+
req.method !== "POST" &&
44+
!(url.pathname === "/mutate" || url.pathname === "/queries")
45+
)
46+
return new Response("Not found", { headers, status: 404 })
47+
48+
try {
49+
const cookieRaw =
50+
req.headers.get("Cookie") ??
51+
// Using `Authorization` header for test compat
52+
req.headers
53+
.get("authorization")
54+
?.replace("Bearer ", "") ??
55+
null
56+
const cookie = await Cookie.parseSigned(
57+
cookieRaw,
58+
env.AUTH_SECRET,
59+
"ztest.auth",
60+
)
61+
const authData = cookie
62+
? z.decode(jsonCodec(AuthData), cookie)
63+
: undefined
64+
65+
if (url.pathname === "/queries") {
66+
const result = await handleGetQueriesRequest(
67+
(name, args) => {
68+
const q = validated[name]
69+
if (!q) throw new Error(`No such query: ${name}`)
70+
return { query: q(authData, ...args) }
71+
},
72+
schema,
73+
req,
74+
)
75+
return Response.json(result, { headers, status: 200 })
76+
}
77+
78+
const client = createMutators(authData)
79+
const mutators = {
80+
...client,
81+
} satisfies ServerMutatorDefs
82+
83+
const processor = new PushProcessor(
84+
new ZQLDatabase(
85+
new PostgresJSConnection(postgres(env.DB.connectionString)),
86+
schema,
87+
),
88+
)
89+
const result = await processor.process(mutators, req)
90+
return Response.json(result, { headers, status: 200 })
91+
} catch (error) {
92+
console.error(error)
93+
return new Response("Internal server error", { headers, status: 500 })
94+
}
95+
},
96+
} satisfies ExportedHandler<Pick<Env, "APP_URL" | "AUTH_SECRET" | "DB">>
97+
98+
const validated = Object.fromEntries(
99+
[
100+
queries.me,
101+
queries.repositoriesForAccount,
102+
// only used for testing
103+
queries.getRepository,
104+
].map((q) => [q.queryName, withValidation(q)]),
105+
)
106+
107+
type ServerMutatorDefs = {
108+
[namespaceOrKey in keyof mutators]: mutators[namespaceOrKey] extends CustomMutatorFn<
109+
infer schema,
110+
infer transaction,
111+
infer args
112+
>
113+
? CustomServerMutatorImpl<schema, transaction, args>
114+
: {
115+
[key in keyof mutators[namespaceOrKey]]: mutators[namespaceOrKey][key] extends CustomMutatorFn<
116+
infer schema,
117+
infer transaction,
118+
infer args
119+
>
120+
? CustomServerMutatorImpl<schema, transaction, args>
121+
: never
122+
}
123+
}
124+
type CustomServerMutatorImpl<schema extends Schema, transaction, args> = (
125+
tx: ServerTransaction<schema, transaction>,
126+
args: args,
127+
) => Promise<void>

0 commit comments

Comments
 (0)