create-katajs 0.1.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/index.js +568 -0
- package/dist/templates/api/.github/workflows/deploy.yml +26 -0
- package/dist/templates/api/README.md +95 -0
- package/dist/templates/api/_dev.vars.example +3 -0
- package/dist/templates/api/_gitignore +11 -0
- package/dist/templates/api/drizzle.config.ts +10 -0
- package/dist/templates/api/package.json +32 -0
- package/dist/templates/api/scripts/graph.ts +19 -0
- package/dist/templates/api/scripts/modules.ts +7 -0
- package/dist/templates/api/src/app.ts +39 -0
- package/dist/templates/api/src/db/schema.ts +11 -0
- package/dist/templates/api/src/index.ts +6 -0
- package/dist/templates/api/src/modules/posts/index.ts +31 -0
- package/dist/templates/api/src/modules/posts/posts.errors.ts +15 -0
- package/dist/templates/api/src/modules/posts/posts.repository.ts +22 -0
- package/dist/templates/api/src/modules/posts/posts.routes.ts +20 -0
- package/dist/templates/api/src/modules/posts/posts.schema.ts +12 -0
- package/dist/templates/api/src/modules/posts/posts.service.ts +26 -0
- package/dist/templates/api/src/types.d.ts +31 -0
- package/dist/templates/api/tsconfig.json +22 -0
- package/dist/templates/api/wrangler.jsonc +35 -0
- package/dist/templates/auth-snippets/api/src/db/auth-schema.ts +53 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.config.ts +30 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.errors.ts +10 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.middleware.ts +37 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.routes.ts +7 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/index.ts +28 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.errors.ts +10 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.middleware.ts +37 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.routes.ts +7 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/index.ts +29 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/package.json +22 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/src/auth.ts +40 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/src/index.ts +2 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/tsconfig.json +4 -0
- package/dist/templates/auth-snippets/monorepo/packages/db/src/auth-schema.ts +53 -0
- package/dist/templates/monorepo/.github/workflows/deploy.yml +39 -0
- package/dist/templates/monorepo/README.md +91 -0
- package/dist/templates/monorepo/_gitignore +12 -0
- package/dist/templates/monorepo/apps/api/_dev.vars.example +7 -0
- package/dist/templates/monorepo/apps/api/_gitignore +7 -0
- package/dist/templates/monorepo/apps/api/package.json +36 -0
- package/dist/templates/monorepo/apps/api/scripts/graph.ts +19 -0
- package/dist/templates/monorepo/apps/api/scripts/modules.ts +7 -0
- package/dist/templates/monorepo/apps/api/src/app.ts +42 -0
- package/dist/templates/monorepo/apps/api/src/index.ts +7 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/index.ts +31 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.errors.ts +15 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.repository.ts +22 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.routes.ts +20 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.schema.ts +13 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.service.ts +26 -0
- package/dist/templates/monorepo/apps/api/src/types.d.ts +29 -0
- package/dist/templates/monorepo/apps/api/tsconfig.json +7 -0
- package/dist/templates/monorepo/apps/api/wrangler.jsonc +34 -0
- package/dist/templates/monorepo/package.json +24 -0
- package/dist/templates/monorepo/packages/api-client/package.json +20 -0
- package/dist/templates/monorepo/packages/api-client/src/index.ts +43 -0
- package/dist/templates/monorepo/packages/api-client/tsconfig.json +4 -0
- package/dist/templates/monorepo/packages/db/drizzle.config.ts +12 -0
- package/dist/templates/monorepo/packages/db/package.json +26 -0
- package/dist/templates/monorepo/packages/db/src/index.ts +13 -0
- package/dist/templates/monorepo/packages/db/src/schema.ts +12 -0
- package/dist/templates/monorepo/packages/db/tsconfig.json +4 -0
- package/dist/templates/monorepo/pnpm-workspace.yaml +3 -0
- package/dist/templates/monorepo/tsconfig.base.json +18 -0
- package/dist/templates/monorepo/turbo.json +19 -0
- package/dist/templates/monorepo-worker/apps/worker/_dev.vars.example +4 -0
- package/dist/templates/monorepo-worker/apps/worker/_gitignore +6 -0
- package/dist/templates/monorepo-worker/apps/worker/package.json +29 -0
- package/dist/templates/monorepo-worker/apps/worker/src/app.ts +27 -0
- package/dist/templates/monorepo-worker/apps/worker/src/index.ts +6 -0
- package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.consumer.ts +23 -0
- package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.service.ts +18 -0
- package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/index.ts +24 -0
- package/dist/templates/monorepo-worker/apps/worker/src/types.d.ts +19 -0
- package/dist/templates/monorepo-worker/apps/worker/tsconfig.json +7 -0
- package/dist/templates/monorepo-worker/apps/worker/wrangler.jsonc +28 -0
- package/package.json +54 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineMiddleware } from '@katajs/core';
|
|
2
|
+
import type { AuthInstance } from '@{{PROJECT_NAME}}/auth';
|
|
3
|
+
import { UnauthorizedError } from './auth.errors';
|
|
4
|
+
|
|
5
|
+
type SessionUser = {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
declare module 'hono' {
|
|
12
|
+
interface ContextVariableMap {
|
|
13
|
+
user: SessionUser;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Attach the current user (if any) to `c.var.user`. Routes that require
|
|
19
|
+
* auth should use `requireAuthMiddleware` instead.
|
|
20
|
+
*/
|
|
21
|
+
export const attachUserMiddleware = (auth: AuthInstance) =>
|
|
22
|
+
defineMiddleware(async (c, next) => {
|
|
23
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
24
|
+
if (session?.user) {
|
|
25
|
+
c.set('user', session.user as SessionUser);
|
|
26
|
+
}
|
|
27
|
+
await next();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Require an authenticated user — throws `UnauthorizedError` if absent. */
|
|
31
|
+
export const requireAuthMiddleware = (auth: AuthInstance) =>
|
|
32
|
+
defineMiddleware(async (c, next) => {
|
|
33
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
34
|
+
if (!session?.user) throw new UnauthorizedError();
|
|
35
|
+
c.set('user', session.user as SessionUser);
|
|
36
|
+
await next();
|
|
37
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineModule } from '@katajs/core';
|
|
2
|
+
import * as schema from '@{{PROJECT_NAME}}/db';
|
|
3
|
+
import type { DrizzleClient } from '@katajs/drizzle';
|
|
4
|
+
import { createAuth, type AuthInstance } from '@{{PROJECT_NAME}}/auth';
|
|
5
|
+
import { authRoutes } from './auth.routes';
|
|
6
|
+
|
|
7
|
+
export const authModule = defineModule({
|
|
8
|
+
name: 'auth',
|
|
9
|
+
provides: {
|
|
10
|
+
authInstance: (c): AuthInstance =>
|
|
11
|
+
createAuth(
|
|
12
|
+
c.db as DrizzleClient<typeof schema>,
|
|
13
|
+
c.env as { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string },
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
requires: [] as const,
|
|
17
|
+
routes: authRoutes,
|
|
18
|
+
prefix: '/auth',
|
|
19
|
+
// katajs:module-consumer
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Services this module contributes to the container's `Registry`. */
|
|
23
|
+
export type AuthRegistry = {
|
|
24
|
+
authInstance: AuthInstance;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type { AuthInstance } from '@{{PROJECT_NAME}}/auth';
|
|
28
|
+
export { UnauthorizedError } from './auth.errors';
|
|
29
|
+
export { attachUserMiddleware, requireAuthMiddleware } from './auth.middleware';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{PROJECT_NAME}}/auth",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@{{PROJECT_NAME}}/db": "workspace:*",
|
|
16
|
+
"@katajs/drizzle": "^0.1.0",
|
|
17
|
+
"better-auth": "^1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.6.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth';
|
|
2
|
+
import { drizzleAdapter as betterAuthDrizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
3
|
+
import type { DrizzleClient } from '@katajs/drizzle';
|
|
4
|
+
import * as schema from '@{{PROJECT_NAME}}/db';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build a Better Auth instance bound to a request-scoped Drizzle client.
|
|
8
|
+
* Called per-request from the auth module's `provides` factory.
|
|
9
|
+
*
|
|
10
|
+
* Note: Better Auth has its *own* `drizzleAdapter` (imported as
|
|
11
|
+
* `betterAuthDrizzleAdapter`) — distinct from `@katajs/drizzle`'s
|
|
12
|
+
* `drizzleAdapter`. The naming overlap is unavoidable.
|
|
13
|
+
*/
|
|
14
|
+
export function createAuth(
|
|
15
|
+
db: DrizzleClient<typeof schema>,
|
|
16
|
+
env: { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string },
|
|
17
|
+
) {
|
|
18
|
+
return betterAuth({
|
|
19
|
+
database: betterAuthDrizzleAdapter(db, {
|
|
20
|
+
provider: 'pg',
|
|
21
|
+
schema: {
|
|
22
|
+
user: schema.user,
|
|
23
|
+
session: schema.session,
|
|
24
|
+
account: schema.account,
|
|
25
|
+
verification: schema.verification,
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
29
|
+
baseURL: env.BETTER_AUTH_URL,
|
|
30
|
+
emailAndPassword: { enabled: true },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AuthInstance = ReturnType<typeof createAuth>;
|
|
35
|
+
|
|
36
|
+
export type SessionUser = {
|
|
37
|
+
id: string;
|
|
38
|
+
email: string;
|
|
39
|
+
name: string;
|
|
40
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Better Auth tables. The exact column shape is dictated by Better Auth's
|
|
5
|
+
* Drizzle adapter — keep these in sync with `better-auth` releases. Run
|
|
6
|
+
* `pnpm db:generate` (from the workspace root) after changes.
|
|
7
|
+
*/
|
|
8
|
+
export const user = pgTable('user', {
|
|
9
|
+
id: text('id').primaryKey(),
|
|
10
|
+
name: text('name').notNull(),
|
|
11
|
+
email: text('email').notNull().unique(),
|
|
12
|
+
emailVerified: boolean('email_verified').notNull().default(false),
|
|
13
|
+
image: text('image'),
|
|
14
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
15
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const session = pgTable('session', {
|
|
19
|
+
id: text('id').primaryKey(),
|
|
20
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
21
|
+
token: text('token').notNull().unique(),
|
|
22
|
+
ipAddress: text('ip_address'),
|
|
23
|
+
userAgent: text('user_agent'),
|
|
24
|
+
userId: text('user_id')
|
|
25
|
+
.notNull()
|
|
26
|
+
.references(() => user.id, { onDelete: 'cascade' }),
|
|
27
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
28
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const account = pgTable('account', {
|
|
32
|
+
id: text('id').primaryKey(),
|
|
33
|
+
accountId: text('account_id').notNull(),
|
|
34
|
+
providerId: text('provider_id').notNull(),
|
|
35
|
+
userId: text('user_id')
|
|
36
|
+
.notNull()
|
|
37
|
+
.references(() => user.id, { onDelete: 'cascade' }),
|
|
38
|
+
accessToken: text('access_token'),
|
|
39
|
+
refreshToken: text('refresh_token'),
|
|
40
|
+
idToken: text('id_token'),
|
|
41
|
+
password: text('password'),
|
|
42
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
43
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const verification = pgTable('verification', {
|
|
47
|
+
id: text('id').primaryKey(),
|
|
48
|
+
identifier: text('identifier').notNull(),
|
|
49
|
+
value: text('value').notNull(),
|
|
50
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
51
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
52
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
53
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: deploy-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
deploy:
|
|
14
|
+
name: Typecheck + deploy apps/api
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: pnpm/action-setup@v4
|
|
22
|
+
with:
|
|
23
|
+
version: 9
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: 20
|
|
28
|
+
cache: pnpm
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: pnpm install --frozen-lockfile
|
|
32
|
+
|
|
33
|
+
- name: Typecheck
|
|
34
|
+
run: pnpm typecheck
|
|
35
|
+
|
|
36
|
+
- name: Deploy
|
|
37
|
+
run: pnpm --filter @{{PROJECT_NAME}}/api deploy
|
|
38
|
+
env:
|
|
39
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A katajs monorepo. One Hono+Workers API, a typed RPC client, and a shared Drizzle schema package.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{PROJECT_NAME}}/
|
|
9
|
+
├── apps/
|
|
10
|
+
│ └── api/ ← Hono + Cloudflare Workers app (deploys to CF)
|
|
11
|
+
├── packages/
|
|
12
|
+
│ ├── db/ ← Drizzle schema + migrations (shared)
|
|
13
|
+
│ └── api-client/ ← typed Hono RPC client (export AppType + factory)
|
|
14
|
+
├── pnpm-workspace.yaml
|
|
15
|
+
├── turbo.json
|
|
16
|
+
└── package.json ← delegating scripts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Why this layout: schema and client types are shared across the workspace; deployable units (the API today, queue workers tomorrow) live in `apps/`. Both consume the packages via `workspace:*` protocol.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Install all workspace deps
|
|
25
|
+
pnpm install
|
|
26
|
+
|
|
27
|
+
# Generate migration SQL from the Drizzle schema
|
|
28
|
+
pnpm db:generate
|
|
29
|
+
|
|
30
|
+
# Apply migrations to your local Postgres (set up your .dev.vars first)
|
|
31
|
+
pnpm db:migrate
|
|
32
|
+
|
|
33
|
+
# Run the API in dev (Wrangler watches packages/db too)
|
|
34
|
+
pnpm dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`http://localhost:8787/health` should return `{ ok: true }` once the API is up.
|
|
38
|
+
|
|
39
|
+
## Common commands
|
|
40
|
+
|
|
41
|
+
| Command | What it does |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `pnpm dev` | `turbo run dev` — runs `wrangler dev` in `apps/api` |
|
|
44
|
+
| `pnpm typecheck` | typechecks every package in parallel (cached) |
|
|
45
|
+
| `pnpm test` | runs vitest in every package that has tests |
|
|
46
|
+
| `pnpm graph` | regenerates `apps/api/graph.html` from the module graph |
|
|
47
|
+
| `pnpm db:generate` | drizzle-kit generate in `packages/db` |
|
|
48
|
+
| `pnpm db:migrate` | drizzle-kit migrate against `packages/db/drizzle.config.ts` |
|
|
49
|
+
| `pnpm deploy` | `wrangler deploy` in `apps/api` |
|
|
50
|
+
|
|
51
|
+
## Adding modules and services
|
|
52
|
+
|
|
53
|
+
The katajs CLI works inside the API:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd apps/api
|
|
57
|
+
pnpm katajs add module billing
|
|
58
|
+
pnpm katajs add service archived --in posts
|
|
59
|
+
pnpm katajs add route delete /:id --in users
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or from the root, the CLI walks down to find the API project automatically (planned for Phase 2).
|
|
63
|
+
|
|
64
|
+
## Using the API client elsewhere
|
|
65
|
+
|
|
66
|
+
When you build a frontend (TanStack Start, Next.js, etc.) or a separate worker that calls the API, depend on `@{{PROJECT_NAME}}/api-client`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createApiClient } from '@{{PROJECT_NAME}}/api-client';
|
|
70
|
+
import type { AppType } from '@{{PROJECT_NAME}}/api';
|
|
71
|
+
|
|
72
|
+
const api = createApiClient<AppType>('https://api.example.com');
|
|
73
|
+
const res = await api.posts.$post({ json: { title: 't', body: 'b' } });
|
|
74
|
+
const data = await res.json(); // typed end-to-end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The client is generic over the API's `AppType`, so the package itself has zero workspace coupling — the consumer brings its own `AppType`. This is also the home for future cross-cutting client concerns (auth header injection, retry policy, error parsing).
|
|
78
|
+
|
|
79
|
+
## What's where
|
|
80
|
+
|
|
81
|
+
- **Drizzle schema** lives in `packages/db/src/schema.ts`. All apps import it from `@{{PROJECT_NAME}}/db`.
|
|
82
|
+
- **Migration SQL** lives in `packages/db/migrations/` (generated by `drizzle-kit`).
|
|
83
|
+
- **Drizzle config** lives in `packages/db/drizzle.config.ts`. Reads connection string from env.
|
|
84
|
+
- **Modules** live in `apps/api/src/modules/`.
|
|
85
|
+
- **Hono app + `createApp({...})`** lives in `apps/api/src/app.ts`.
|
|
86
|
+
|
|
87
|
+
## Deploy
|
|
88
|
+
|
|
89
|
+
GitHub Actions workflow at `.github/workflows/deploy.yml` typechecks and deploys `apps/api` on push to `main`. You'll need a `CLOUDFLARE_API_TOKEN` secret with permissions to deploy your Worker.
|
|
90
|
+
|
|
91
|
+
For migrations, run `pnpm db:migrate` against your production database from your CI or a one-shot local command — it's not currently part of the deploy workflow.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Local-only secrets for `wrangler dev`. Copy this file to `.dev.vars`
|
|
2
|
+
# and fill in real values. The `.dev.vars` file is gitignored.
|
|
3
|
+
#
|
|
4
|
+
# DATABASE_URL is read by drizzle-kit (run from packages/db) for migrations.
|
|
5
|
+
# Wrangler reads HYPERDRIVE.localConnectionString from wrangler.jsonc instead.
|
|
6
|
+
|
|
7
|
+
DATABASE_URL="postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{PROJECT_NAME}}/api",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "wrangler dev",
|
|
13
|
+
"deploy": "wrangler deploy",
|
|
14
|
+
"graph": "tsx scripts/graph.ts",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@{{PROJECT_NAME}}/db": "workspace:*",
|
|
19
|
+
"@hono/zod-validator": "^0.4.1",
|
|
20
|
+
"@katajs/core": "^0.1.0",
|
|
21
|
+
"@katajs/drizzle": "^0.1.0",
|
|
22
|
+
"drizzle-orm": "^0.36.4",
|
|
23
|
+
"hono": "^4.6.10",
|
|
24
|
+
"pg": "^8.13.1",
|
|
25
|
+
"zod": "^3.23.8"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@cloudflare/workers-types": "^4.20241112.0",
|
|
29
|
+
"@katajs/cli": "^0.1.0",
|
|
30
|
+
"@types/node": "^22.9.0",
|
|
31
|
+
"@types/pg": "^8.11.10",
|
|
32
|
+
"tsx": "^4.19.2",
|
|
33
|
+
"typescript": "^5.6.3",
|
|
34
|
+
"wrangler": "^3.86.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { inspectModules } from '@katajs/core';
|
|
5
|
+
|
|
6
|
+
import { modules } from './modules';
|
|
7
|
+
|
|
8
|
+
const here = fileURLToPath(new URL('.', import.meta.url));
|
|
9
|
+
const out = resolve(here, '..', 'graph.html');
|
|
10
|
+
|
|
11
|
+
const insp = inspectModules(modules);
|
|
12
|
+
writeFileSync(out, insp.html({ title: '{{PROJECT_NAME}} — module graph' }));
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log(`✓ wrote ${out}`);
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.log(
|
|
18
|
+
` ${insp.modules.length} modules • ${insp.edges.length} dependencies • ${insp.routes.length} routes`,
|
|
19
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createApp, type RequestVariables } from '@katajs/core';
|
|
2
|
+
import { drizzleAdapter } from '@katajs/drizzle';
|
|
3
|
+
import * as schema from '@{{PROJECT_NAME}}/db';
|
|
4
|
+
|
|
5
|
+
import { postsModule } from './modules/posts/index';
|
|
6
|
+
// katajs:module-imports
|
|
7
|
+
|
|
8
|
+
export type Bindings = {
|
|
9
|
+
HYPERDRIVE: Hyperdrive;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AppEnv = {
|
|
13
|
+
Bindings: Bindings;
|
|
14
|
+
Variables: RequestVariables;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const { app, queue } = createApp({
|
|
18
|
+
bindings: {} as Bindings,
|
|
19
|
+
db: drizzleAdapter({ schema }),
|
|
20
|
+
modules: [
|
|
21
|
+
postsModule,
|
|
22
|
+
// katajs:modules
|
|
23
|
+
],
|
|
24
|
+
// Producer manifest. Each entry surfaces a typed wrapper at
|
|
25
|
+
// c.var.queues.<name>.send(body), validated against the schema.
|
|
26
|
+
queues: {
|
|
27
|
+
// katajs:queues
|
|
28
|
+
},
|
|
29
|
+
// Define your HTTP surface here. Add `.get()` / `.post()` for ad-hoc routes
|
|
30
|
+
// (health checks, webhooks) and `.route(prefix, module.routes)` per module.
|
|
31
|
+
routes: (base) =>
|
|
32
|
+
base
|
|
33
|
+
.get('/health', (c) =>
|
|
34
|
+
c.json({ ok: true, requestId: c.var.requestId }),
|
|
35
|
+
)
|
|
36
|
+
.route(postsModule.prefix, postsModule.routes),
|
|
37
|
+
// katajs:routes
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export { queue };
|
|
41
|
+
export default app;
|
|
42
|
+
export type AppType = typeof app;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import app, { queue } from './app';
|
|
2
|
+
|
|
3
|
+
// Worker default export. `queue` is undefined when no module declares a
|
|
4
|
+
// `consumer:` field; Cloudflare Workers handles either case fine.
|
|
5
|
+
export default { fetch: app.fetch, queue };
|
|
6
|
+
|
|
7
|
+
export type { AppType, AppEnv, Bindings } from './app';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineModule } from '@katajs/core';
|
|
2
|
+
import { makePostRepository, type PostRepository } from './posts.repository';
|
|
3
|
+
import { makePostService, type PostsService } from './posts.service';
|
|
4
|
+
// katajs:module-service-imports
|
|
5
|
+
import { postsRoutes } from './posts.routes';
|
|
6
|
+
|
|
7
|
+
export const postsModule = defineModule({
|
|
8
|
+
name: 'posts',
|
|
9
|
+
provides: {
|
|
10
|
+
postRepository: (c): PostRepository => makePostRepository(c.db),
|
|
11
|
+
postService: (c): PostsService => makePostService(c),
|
|
12
|
+
// katajs:module-provides
|
|
13
|
+
},
|
|
14
|
+
requires: [] as const,
|
|
15
|
+
routes: postsRoutes,
|
|
16
|
+
prefix: '/posts',
|
|
17
|
+
// katajs:module-consumer
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Services this module contributes to the container's `Registry`. Compose it
|
|
22
|
+
* in `app.ts` via `interface Registry extends PostsRegistry {}`. Adding a new
|
|
23
|
+
* module is one extra `extends` clause — no key-by-key bookkeeping.
|
|
24
|
+
*/
|
|
25
|
+
export type PostsRegistry = {
|
|
26
|
+
postRepository: PostRepository;
|
|
27
|
+
postService: PostsService;
|
|
28
|
+
// katajs:module-registry
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type { PostRepository, PostsService };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AppError } from '@katajs/core';
|
|
2
|
+
|
|
3
|
+
export class PostNotFoundError extends AppError {
|
|
4
|
+
override readonly status = 404;
|
|
5
|
+
override readonly code = 'post_not_found';
|
|
6
|
+
override readonly publicMessage = 'Post not found';
|
|
7
|
+
|
|
8
|
+
constructor(public readonly postId: string) {
|
|
9
|
+
super(`Post ${postId} not found`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override get publicPayload() {
|
|
13
|
+
return { postId: this.postId };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import type { DrizzleClientOrTx } from '@katajs/drizzle';
|
|
3
|
+
import { posts, type NewPost, type Post } from '@{{PROJECT_NAME}}/db';
|
|
4
|
+
import * as schema from '@{{PROJECT_NAME}}/db';
|
|
5
|
+
|
|
6
|
+
type Schema = typeof schema;
|
|
7
|
+
|
|
8
|
+
export function makePostRepository(dbOrTx: DrizzleClientOrTx<Schema>) {
|
|
9
|
+
return {
|
|
10
|
+
async findById(id: string): Promise<Post | undefined> {
|
|
11
|
+
const rows = await dbOrTx.select().from(posts).where(eq(posts.id, id)).limit(1);
|
|
12
|
+
return rows[0];
|
|
13
|
+
},
|
|
14
|
+
async insert(input: NewPost): Promise<Post> {
|
|
15
|
+
const [row] = await dbOrTx.insert(posts).values(input).returning();
|
|
16
|
+
if (!row) throw new Error('insert returned no row');
|
|
17
|
+
return row;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type PostRepository = ReturnType<typeof makePostRepository>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { validate } from '@katajs/core';
|
|
3
|
+
import type { AppEnv } from '../../app';
|
|
4
|
+
import { CreatePostSchema, PostIdParam } from './posts.schema';
|
|
5
|
+
|
|
6
|
+
export const postsRoutes = new Hono<AppEnv>()
|
|
7
|
+
.get('/:id', ...validate({ param: PostIdParam }), async (c) => {
|
|
8
|
+
const { id } = c.req.valid('param');
|
|
9
|
+
const service = c.var.resolve('postService');
|
|
10
|
+
const post = await service.getById(id);
|
|
11
|
+
return c.json({ post });
|
|
12
|
+
})
|
|
13
|
+
.post('/', ...validate({ body: CreatePostSchema }), async (c) => {
|
|
14
|
+
const input = c.req.valid('json');
|
|
15
|
+
const service = c.var.resolve('postService');
|
|
16
|
+
const post = await service.create(input);
|
|
17
|
+
return c.json({ post }, 201);
|
|
18
|
+
})
|
|
19
|
+
// katajs:module-routes
|
|
20
|
+
;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const CreatePostSchema = z.object({
|
|
4
|
+
title: z.string().min(1).max(200),
|
|
5
|
+
body: z.string().min(1).max(10_000),
|
|
6
|
+
authorId: z.string().min(1),
|
|
7
|
+
});
|
|
8
|
+
export type CreatePostInput = z.infer<typeof CreatePostSchema>;
|
|
9
|
+
|
|
10
|
+
export const PostIdParam = z.object({
|
|
11
|
+
id: z.string().uuid(),
|
|
12
|
+
});
|
|
13
|
+
export type PostIdParamInput = z.infer<typeof PostIdParam>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RequestContainer } from '@katajs/core';
|
|
2
|
+
import { PostNotFoundError } from './posts.errors';
|
|
3
|
+
import type { CreatePostInput } from './posts.schema';
|
|
4
|
+
import type { Post } from '@{{PROJECT_NAME}}/db';
|
|
5
|
+
|
|
6
|
+
export type PostsService = {
|
|
7
|
+
getById(id: string): Promise<Post>;
|
|
8
|
+
create(input: CreatePostInput): Promise<Post>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function makePostService(c: RequestContainer): PostsService {
|
|
12
|
+
return {
|
|
13
|
+
async getById(id) {
|
|
14
|
+
const repo = c.resolve('postRepository');
|
|
15
|
+
const post = await repo.findById(id);
|
|
16
|
+
if (!post) throw new PostNotFoundError(id);
|
|
17
|
+
return post;
|
|
18
|
+
},
|
|
19
|
+
async create(input) {
|
|
20
|
+
return c.withTransaction(async (tx) => {
|
|
21
|
+
const repo = tx.resolve('postRepository');
|
|
22
|
+
return repo.insert(input);
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module augmentations for `@katajs/core`. Picked up automatically by
|
|
3
|
+
* tsconfig's `include` glob — no runtime import needed. Co-locate everything
|
|
4
|
+
* that teaches the framework about this app's concrete types here.
|
|
5
|
+
*
|
|
6
|
+
* The `katajs:registry-imports` and `katajs:registry` markers below are used
|
|
7
|
+
* by `katajs add module` to insert new module registry entries automatically.
|
|
8
|
+
*/
|
|
9
|
+
import type { DrizzleClient } from '@katajs/drizzle';
|
|
10
|
+
import type * as schema from '@{{PROJECT_NAME}}/db';
|
|
11
|
+
import type { Bindings } from './app';
|
|
12
|
+
|
|
13
|
+
import type { PostsRegistry } from './modules/posts/index';
|
|
14
|
+
// katajs:registry-imports
|
|
15
|
+
|
|
16
|
+
declare module '@katajs/core' {
|
|
17
|
+
interface AppDb extends DrizzleClient<typeof schema> {}
|
|
18
|
+
interface AppEnv extends Bindings {}
|
|
19
|
+
interface Registry
|
|
20
|
+
extends PostsRegistry
|
|
21
|
+
// katajs:registry
|
|
22
|
+
{}
|
|
23
|
+
|
|
24
|
+
// Typed producer queues. Each entry maps a queue name to its body type so
|
|
25
|
+
// c.var.queues.<name>.send(body) is fully typed.
|
|
26
|
+
interface QueuesRegistry {
|
|
27
|
+
// katajs:queues-registry
|
|
28
|
+
}
|
|
29
|
+
}
|