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,95 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
Scaffolded with [`create-katajs`](https://github.com/yasr/katajs).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Spin up a local Postgres for development (Docker example):
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
docker run -d --name {{PROJECT_NAME}}-pg \
|
|
15
|
+
-e POSTGRES_PASSWORD=postgres \
|
|
16
|
+
-e POSTGRES_DB={{PROJECT_NAME}}_dev \
|
|
17
|
+
-p 5432:5432 \
|
|
18
|
+
postgres:16
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Copy `.dev.vars.example` to `.dev.vars` (for `wrangler dev`).
|
|
22
|
+
|
|
23
|
+
## Develop
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm db:generate # generate Drizzle migrations after schema changes
|
|
27
|
+
pnpm db:migrate # apply migrations to your dev Postgres
|
|
28
|
+
pnpm dev # wrangler dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`wrangler dev` connects directly to the Postgres URL in `.dev.vars` (or
|
|
32
|
+
`localConnectionString` in `wrangler.jsonc`). Hyperdrive's pooling/caching
|
|
33
|
+
applies on Cloudflare's edge in production; locally you get a transparent
|
|
34
|
+
passthrough.
|
|
35
|
+
|
|
36
|
+
## Module graph
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pnpm graph
|
|
40
|
+
# writes graph.html — open in any browser
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Generates a self-contained HTML page showing your modules, their dependency
|
|
44
|
+
edges, and a flat routes table. Useful for reviewing your app's structure or
|
|
45
|
+
embedding in PR descriptions. Regenerate after structural changes.
|
|
46
|
+
|
|
47
|
+
## Type-safe clients with Hono RPC
|
|
48
|
+
|
|
49
|
+
`AppType` is exported from `src/app.ts`. Consume it from another package:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { hc } from 'hono/client';
|
|
53
|
+
import type { AppType } from '<your-api-package>';
|
|
54
|
+
|
|
55
|
+
const client = hc<AppType>('https://api.example.com');
|
|
56
|
+
const res = await client.posts.$post({ json: { title: 'Hi', body: 'Hello' } });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Adding tests
|
|
60
|
+
|
|
61
|
+
If you add unit tests with synthetic services (e.g. test fixtures that
|
|
62
|
+
augment `@katajs/core`'s `Registry` with throwaway service keys), keep them
|
|
63
|
+
in their own tsconfig so test-only augmentations don't pollute editor
|
|
64
|
+
autocomplete in `src/`. TypeScript module augmentations are global per
|
|
65
|
+
program — splitting the program is the only fix.
|
|
66
|
+
|
|
67
|
+
Pattern:
|
|
68
|
+
|
|
69
|
+
```jsonc
|
|
70
|
+
// tsconfig.json — for editor + `pnpm typecheck`
|
|
71
|
+
{ "include": ["src/**/*"] }
|
|
72
|
+
|
|
73
|
+
// tsconfig.test.json — for `pnpm test:typecheck`
|
|
74
|
+
{
|
|
75
|
+
"extends": "./tsconfig.json",
|
|
76
|
+
"include": ["src/**/*", "test/**/*"]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run both in your `typecheck` script:
|
|
81
|
+
```json
|
|
82
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Deploy
|
|
86
|
+
|
|
87
|
+
CI pushes to `main` deploy via the included workflow. Locally:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
pnpm deploy
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
You'll need `CLOUDFLARE_API_TOKEN` in CI secrets and a real Hyperdrive
|
|
94
|
+
configuration before the first deploy. See
|
|
95
|
+
[Cloudflare Hyperdrive docs](https://developers.cloudflare.com/hyperdrive).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Local connection string used by Hyperdrive in `wrangler dev`.
|
|
2
|
+
# Wrangler reads this env var if set; it overrides `localConnectionString` in wrangler.jsonc.
|
|
3
|
+
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from 'drizzle-kit';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
dialect: 'postgresql',
|
|
5
|
+
schema: './src/db/schema.ts',
|
|
6
|
+
out: './drizzle',
|
|
7
|
+
dbCredentials: {
|
|
8
|
+
url: process.env.DATABASE_URL ?? 'postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"graph": "tsx scripts/graph.ts",
|
|
10
|
+
"db:generate": "drizzle-kit generate",
|
|
11
|
+
"db:migrate": "drizzle-kit migrate",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@hono/zod-validator": "^0.4.1",
|
|
16
|
+
"@katajs/core": "^0.1.0",
|
|
17
|
+
"@katajs/drizzle": "^0.1.0",
|
|
18
|
+
"drizzle-orm": "^0.36.4",
|
|
19
|
+
"hono": "^4.6.10",
|
|
20
|
+
"pg": "^8.13.1",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@cloudflare/workers-types": "^4.20241112.0",
|
|
25
|
+
"@types/node": "^22.9.0",
|
|
26
|
+
"@types/pg": "^8.11.10",
|
|
27
|
+
"drizzle-kit": "^0.28.1",
|
|
28
|
+
"tsx": "^4.19.2",
|
|
29
|
+
"typescript": "^5.6.3",
|
|
30
|
+
"wrangler": "^3.86.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -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,39 @@
|
|
|
1
|
+
import { createApp, type RequestVariables } from '@katajs/core';
|
|
2
|
+
import { drizzleAdapter } from '@katajs/drizzle';
|
|
3
|
+
import * as schema from './db/schema';
|
|
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
|
+
.route(postsModule.prefix, postsModule.routes),
|
|
34
|
+
// katajs:routes
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export { queue };
|
|
38
|
+
export default app;
|
|
39
|
+
export type AppType = typeof app;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
2
|
+
|
|
3
|
+
export const posts = pgTable('posts', {
|
|
4
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
5
|
+
title: text('title').notNull(),
|
|
6
|
+
body: text('body').notNull(),
|
|
7
|
+
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type Post = typeof posts.$inferSelect;
|
|
11
|
+
export type NewPost = typeof posts.$inferInsert;
|
|
@@ -0,0 +1,6 @@
|
|
|
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. When
|
|
5
|
+
// you add your first consumer, no change to this file is needed.
|
|
6
|
+
export default { fetch: app.fetch, queue };
|
|
@@ -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 '../../db/schema';
|
|
4
|
+
import * as schema from '../../db/schema';
|
|
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,12 @@
|
|
|
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
|
+
});
|
|
7
|
+
export type CreatePostInput = z.infer<typeof CreatePostSchema>;
|
|
8
|
+
|
|
9
|
+
export const PostIdParam = z.object({
|
|
10
|
+
id: z.string().uuid(),
|
|
11
|
+
});
|
|
12
|
+
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 '../../db/schema';
|
|
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,31 @@
|
|
|
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
|
+
* If you remove or relocate them, future `add module` runs will print the
|
|
9
|
+
* snippet for you to paste manually.
|
|
10
|
+
*/
|
|
11
|
+
import type { DrizzleClient } from '@katajs/drizzle';
|
|
12
|
+
import type * as schema from './db/schema';
|
|
13
|
+
import type { Bindings } from './app';
|
|
14
|
+
|
|
15
|
+
import type { PostsRegistry } from './modules/posts/index';
|
|
16
|
+
// katajs:registry-imports
|
|
17
|
+
|
|
18
|
+
declare module '@katajs/core' {
|
|
19
|
+
interface AppDb extends DrizzleClient<typeof schema> {}
|
|
20
|
+
interface AppEnv extends Bindings {}
|
|
21
|
+
interface Registry
|
|
22
|
+
extends PostsRegistry
|
|
23
|
+
// katajs:registry
|
|
24
|
+
{}
|
|
25
|
+
|
|
26
|
+
// Typed producer queues. Each entry maps a queue name to its body type so
|
|
27
|
+
// c.var.queues.<name>.send(body) is fully typed.
|
|
28
|
+
interface QueuesRegistry {
|
|
29
|
+
// katajs:queues-registry
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2023"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationMap": true,
|
|
17
|
+
"sourceMap": true,
|
|
18
|
+
"noEmit": true,
|
|
19
|
+
"types": ["@cloudflare/workers-types", "node"]
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*", "scripts/**/*", "drizzle.config.ts"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "{{PROJECT_NAME}}",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"compatibility_date": "{{TODAY_ISO}}",
|
|
6
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
7
|
+
"observability": { "enabled": true },
|
|
8
|
+
"hyperdrive": [
|
|
9
|
+
{
|
|
10
|
+
"binding": "HYPERDRIVE",
|
|
11
|
+
"id": "<your-hyperdrive-id>",
|
|
12
|
+
"localConnectionString": "postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// Cloudflare Queues bindings. Uncomment + adjust when a module in this app
|
|
17
|
+
// declares a `consumer:` field (see docs/concepts/queues.md). Both halves
|
|
18
|
+
// are needed: producers (to send) and consumers (to receive).
|
|
19
|
+
//
|
|
20
|
+
// "queues": {
|
|
21
|
+
// "producers": [
|
|
22
|
+
// { "binding": "ORDER_QUEUE", "queue": "orders" },
|
|
23
|
+
// { "binding": "ORDER_DLQ", "queue": "orders-dlq" }
|
|
24
|
+
// ],
|
|
25
|
+
// "consumers": [
|
|
26
|
+
// {
|
|
27
|
+
// "queue": "orders",
|
|
28
|
+
// "max_batch_size": 100,
|
|
29
|
+
// "max_batch_timeout": 30,
|
|
30
|
+
// "max_retries": 3,
|
|
31
|
+
// "dead_letter_queue": "orders-dlq"
|
|
32
|
+
// }
|
|
33
|
+
// ]
|
|
34
|
+
// }
|
|
35
|
+
}
|
|
@@ -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` 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,30 @@
|
|
|
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 '../../db/schema';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build a Better Auth instance bound to the request-scoped Drizzle client.
|
|
8
|
+
*
|
|
9
|
+
* Note: Better Auth has its *own* `drizzleAdapter` (imported as
|
|
10
|
+
* `betterAuthDrizzleAdapter`) — distinct from `@katajs/drizzle`'s
|
|
11
|
+
* `drizzleAdapter`. The naming overlap is unavoidable.
|
|
12
|
+
*/
|
|
13
|
+
export function makeAuth(db: DrizzleClient<typeof schema>, env: { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string }) {
|
|
14
|
+
return betterAuth({
|
|
15
|
+
database: betterAuthDrizzleAdapter(db, {
|
|
16
|
+
provider: 'pg',
|
|
17
|
+
schema: {
|
|
18
|
+
user: schema.user,
|
|
19
|
+
session: schema.session,
|
|
20
|
+
account: schema.account,
|
|
21
|
+
verification: schema.verification,
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
25
|
+
baseURL: env.BETTER_AUTH_URL,
|
|
26
|
+
emailAndPassword: { enabled: true },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type AuthInstance = ReturnType<typeof makeAuth>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AppError } from '@katajs/core';
|
|
2
|
+
|
|
3
|
+
export class UnauthorizedError extends AppError {
|
|
4
|
+
override readonly status = 401;
|
|
5
|
+
override readonly code = 'unauthorized';
|
|
6
|
+
override readonly publicMessage = 'Authentication required';
|
|
7
|
+
constructor() {
|
|
8
|
+
super('Unauthorized');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineMiddleware } from '@katajs/core';
|
|
2
|
+
import type { AuthInstance } from './auth.config';
|
|
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 auth
|
|
19
|
+
* 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,28 @@
|
|
|
1
|
+
import { defineModule } from '@katajs/core';
|
|
2
|
+
import * as schema from '../../db/schema';
|
|
3
|
+
import type { DrizzleClient } from '@katajs/drizzle';
|
|
4
|
+
import { makeAuth, type AuthInstance } from './auth.config';
|
|
5
|
+
import { authRoutes } from './auth.routes';
|
|
6
|
+
|
|
7
|
+
export const authModule = defineModule({
|
|
8
|
+
name: 'auth',
|
|
9
|
+
provides: {
|
|
10
|
+
authInstance: (c): AuthInstance =>
|
|
11
|
+
makeAuth(
|
|
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
|
+
});
|
|
20
|
+
|
|
21
|
+
/** Services this module contributes to the container's `Registry`. */
|
|
22
|
+
export type AuthRegistry = {
|
|
23
|
+
authInstance: AuthInstance;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type { AuthInstance } from './auth.config';
|
|
27
|
+
export { UnauthorizedError } from './auth.errors';
|
|
28
|
+
export { attachUserMiddleware, requireAuthMiddleware } from './auth.middleware';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AppError } from '@katajs/core';
|
|
2
|
+
|
|
3
|
+
export class UnauthorizedError extends AppError {
|
|
4
|
+
override readonly status = 401;
|
|
5
|
+
override readonly code = 'unauthorized';
|
|
6
|
+
override readonly publicMessage = 'Authentication required';
|
|
7
|
+
constructor() {
|
|
8
|
+
super('Unauthorized');
|
|
9
|
+
}
|
|
10
|
+
}
|