create-kuckit-app 0.1.0 → 0.2.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/dist/bin.js +11 -5
- package/dist/{create-project-DTm05G7D.js → create-project-CP-h4Ygi.js} +7 -5
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/templates/base/.claude/CLAUDE.md +44 -0
- package/templates/base/.claude/agents/daidalos.md +76 -0
- package/templates/base/.claude/agents/episteme.md +79 -0
- package/templates/base/.claude/agents/librarian.md +132 -0
- package/templates/base/.claude/agents/oracle.md +210 -0
- package/templates/base/.claude/commands/create-plan.md +159 -0
- package/templates/base/.claude/commands/file-beads.md +98 -0
- package/templates/base/.claude/commands/review-beads.md +161 -0
- package/templates/base/.claude/settings.json +11 -0
- package/templates/base/.claude/skills/kuckit/SKILL.md +436 -0
- package/templates/base/.claude/skills/kuckit/references/ARCHITECTURE.md +388 -0
- package/templates/base/.claude/skills/kuckit/references/CLI-COMMANDS.md +365 -0
- package/templates/base/.claude/skills/kuckit/references/MODULE-DEVELOPMENT.md +581 -0
- package/templates/base/.claude/skills/kuckit/references/PACKAGES.md +112 -0
- package/templates/base/.claude/skills/kuckit/references/PUBLISHING.md +231 -0
- package/templates/base/.env.example +13 -0
- package/templates/base/.github/workflows/ci.yml +28 -0
- package/templates/base/.husky/pre-commit +1 -0
- package/templates/base/.prettierignore +5 -0
- package/templates/base/.prettierrc +8 -0
- package/templates/base/AGENTS.md +148 -0
- package/templates/base/apps/server/.env.example +18 -0
- package/templates/base/apps/server/AGENTS.md +37 -0
- package/templates/base/apps/server/package.json +13 -4
- package/templates/base/apps/server/src/app.ts +20 -0
- package/templates/base/apps/server/src/auth.ts +10 -0
- package/templates/base/apps/server/src/config/modules.ts +14 -6
- package/templates/base/apps/server/src/container.ts +81 -0
- package/templates/base/apps/server/src/health.ts +27 -0
- package/templates/base/apps/server/src/middleware/container.ts +41 -0
- package/templates/base/apps/server/src/rpc-router-registry.ts +26 -0
- package/templates/base/apps/server/src/rpc.ts +31 -0
- package/templates/base/apps/server/src/server.ts +39 -29
- package/templates/base/apps/web/.env.example +4 -0
- package/templates/base/apps/web/AGENTS.md +53 -0
- package/templates/base/apps/web/index.html +1 -1
- package/templates/base/apps/web/package.json +15 -3
- package/templates/base/apps/web/src/lib/kuckit-router.ts +42 -0
- package/templates/base/apps/web/src/main.tsx +26 -14
- package/templates/base/apps/web/src/providers/KuckitProvider.tsx +147 -0
- package/templates/base/apps/web/src/providers/ServicesProvider.tsx +47 -0
- package/templates/base/apps/web/src/routeTree.gen.ts +91 -0
- package/templates/base/apps/web/src/routes/__root.tsx +31 -0
- package/templates/base/apps/web/src/routes/index.tsx +46 -0
- package/templates/base/apps/web/src/routes/login.tsx +108 -0
- package/templates/base/apps/web/src/services/auth-client.ts +12 -0
- package/templates/base/apps/web/src/services/index.ts +3 -0
- package/templates/base/apps/web/src/services/rpc.ts +29 -0
- package/templates/base/apps/web/src/services/types.ts +14 -0
- package/templates/base/apps/web/vite.config.ts +2 -1
- package/templates/base/docker-compose.yml +23 -0
- package/templates/base/eslint.config.js +18 -0
- package/templates/base/package.json +32 -2
- package/templates/base/packages/api/AGENTS.md +27 -0
- package/templates/base/packages/api/package.json +35 -0
- package/templates/base/packages/api/src/context.ts +48 -0
- package/templates/base/packages/api/src/index.ts +22 -0
- package/templates/base/packages/api/tsconfig.json +8 -0
- package/templates/base/packages/auth/AGENTS.md +45 -0
- package/templates/base/packages/auth/package.json +27 -0
- package/templates/base/packages/auth/src/index.ts +22 -0
- package/templates/base/packages/auth/tsconfig.json +8 -0
- package/templates/base/packages/db/AGENTS.md +59 -0
- package/templates/base/packages/db/drizzle.config.ts +19 -0
- package/templates/base/packages/db/package.json +36 -0
- package/templates/base/packages/db/src/connection.ts +40 -0
- package/templates/base/packages/db/src/index.ts +4 -0
- package/templates/base/packages/db/src/migrations/0000_init.sql +54 -0
- package/templates/base/packages/db/src/migrations/meta/_journal.json +13 -0
- package/templates/base/packages/db/src/schema/auth.ts +51 -0
- package/templates/base/packages/db/tsconfig.json +8 -0
- package/templates/base/packages/items-module/AGENTS.md +112 -0
- package/templates/base/packages/items-module/package.json +32 -0
- package/templates/base/packages/items-module/src/adapters/item.drizzle.ts +66 -0
- package/templates/base/packages/items-module/src/api/items.router.ts +47 -0
- package/templates/base/packages/items-module/src/client-module.ts +39 -0
- package/templates/base/packages/items-module/src/domain/item.entity.ts +36 -0
- package/templates/base/packages/items-module/src/index.ts +15 -0
- package/templates/base/packages/items-module/src/module.ts +53 -0
- package/templates/base/packages/items-module/src/ports/item.repository.ts +13 -0
- package/templates/base/packages/items-module/src/ui/ItemsPage.tsx +162 -0
- package/templates/base/packages/items-module/src/usecases/create-item.ts +25 -0
- package/templates/base/packages/items-module/src/usecases/delete-item.ts +18 -0
- package/templates/base/packages/items-module/src/usecases/get-item.ts +19 -0
- package/templates/base/packages/items-module/src/usecases/list-items.ts +21 -0
- package/templates/base/packages/items-module/tsconfig.json +9 -0
- package/templates/base/turbo.json +13 -1
- package/templates/base/apps/web/src/App.tsx +0 -16
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__APP_NAME_KEBAB__/db",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./*": {
|
|
14
|
+
"types": "./src/*.ts",
|
|
15
|
+
"default": "./src/*.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"db:generate": "drizzle-kit generate",
|
|
20
|
+
"db:migrate": "drizzle-kit migrate",
|
|
21
|
+
"db:studio": "drizzle-kit studio"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"drizzle-kit": "^0.31.2",
|
|
25
|
+
"@types/pg": "^8.11.11"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"typescript": "^5"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"drizzle-orm": "^0.44.2",
|
|
32
|
+
"pg": "^8.14.1",
|
|
33
|
+
"dotenv": "^17.2.2",
|
|
34
|
+
"zod": "^4.1.11"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection utilities
|
|
3
|
+
* Single source of truth for DATABASE_URL construction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build DATABASE_URL from environment variables
|
|
8
|
+
*
|
|
9
|
+
* Supports two modes:
|
|
10
|
+
* 1. Direct DATABASE_URL - used in local development
|
|
11
|
+
* 2. Individual DB_* vars - used in Cloud Run with Cloud SQL Auth Proxy
|
|
12
|
+
*
|
|
13
|
+
* @throws Error if neither DATABASE_URL nor complete DB_* vars are provided
|
|
14
|
+
*/
|
|
15
|
+
export function buildDatabaseUrl(): string {
|
|
16
|
+
// Prefer DATABASE_URL if provided directly
|
|
17
|
+
if (process.env.DATABASE_URL) {
|
|
18
|
+
return process.env.DATABASE_URL
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Construct from individual components (Cloud Run with Cloud SQL)
|
|
22
|
+
const { DB_HOST, DB_USER, DB_PASSWORD, DB_NAME } = process.env
|
|
23
|
+
if (DB_HOST && DB_USER && DB_PASSWORD && DB_NAME) {
|
|
24
|
+
return `postgresql://${DB_USER}:${encodeURIComponent(DB_PASSWORD)}@/${DB_NAME}?host=${DB_HOST}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Missing database configuration: provide DATABASE_URL or DB_HOST, DB_USER, DB_PASSWORD, DB_NAME'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure DATABASE_URL is set in process.env
|
|
34
|
+
* Call this early in application startup to set up the environment
|
|
35
|
+
*/
|
|
36
|
+
export function ensureDatabaseUrl(): string {
|
|
37
|
+
const url = buildDatabaseUrl()
|
|
38
|
+
process.env.DATABASE_URL = url
|
|
39
|
+
return url
|
|
40
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Initial migration: Auth tables
|
|
2
|
+
-- This migration is generated by drizzle-kit
|
|
3
|
+
-- Run: bun run db:migrate
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS "user" (
|
|
6
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
7
|
+
"name" text NOT NULL,
|
|
8
|
+
"email" text NOT NULL,
|
|
9
|
+
"email_verified" boolean NOT NULL,
|
|
10
|
+
"image" text,
|
|
11
|
+
"created_at" timestamp NOT NULL,
|
|
12
|
+
"updated_at" timestamp NOT NULL,
|
|
13
|
+
CONSTRAINT "user_email_unique" UNIQUE("email")
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS "session" (
|
|
17
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
18
|
+
"expires_at" timestamp NOT NULL,
|
|
19
|
+
"token" text NOT NULL,
|
|
20
|
+
"created_at" timestamp NOT NULL,
|
|
21
|
+
"updated_at" timestamp NOT NULL,
|
|
22
|
+
"ip_address" text,
|
|
23
|
+
"user_agent" text,
|
|
24
|
+
"user_id" text NOT NULL,
|
|
25
|
+
CONSTRAINT "session_token_unique" UNIQUE("token")
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS "account" (
|
|
29
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
30
|
+
"account_id" text NOT NULL,
|
|
31
|
+
"provider_id" text NOT NULL,
|
|
32
|
+
"user_id" text NOT NULL,
|
|
33
|
+
"access_token" text,
|
|
34
|
+
"refresh_token" text,
|
|
35
|
+
"id_token" text,
|
|
36
|
+
"access_token_expires_at" timestamp,
|
|
37
|
+
"refresh_token_expires_at" timestamp,
|
|
38
|
+
"scope" text,
|
|
39
|
+
"password" text,
|
|
40
|
+
"created_at" timestamp NOT NULL,
|
|
41
|
+
"updated_at" timestamp NOT NULL
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS "verification" (
|
|
45
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
46
|
+
"identifier" text NOT NULL,
|
|
47
|
+
"value" text NOT NULL,
|
|
48
|
+
"expires_at" timestamp NOT NULL,
|
|
49
|
+
"created_at" timestamp,
|
|
50
|
+
"updated_at" timestamp
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
|
54
|
+
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core'
|
|
2
|
+
|
|
3
|
+
export const user = pgTable('user', {
|
|
4
|
+
id: text('id').primaryKey(),
|
|
5
|
+
name: text('name').notNull(),
|
|
6
|
+
email: text('email').notNull().unique(),
|
|
7
|
+
emailVerified: boolean('email_verified').notNull(),
|
|
8
|
+
image: text('image'),
|
|
9
|
+
createdAt: timestamp('created_at').notNull(),
|
|
10
|
+
updatedAt: timestamp('updated_at').notNull(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const session = pgTable('session', {
|
|
14
|
+
id: text('id').primaryKey(),
|
|
15
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
16
|
+
token: text('token').notNull().unique(),
|
|
17
|
+
createdAt: timestamp('created_at').notNull(),
|
|
18
|
+
updatedAt: timestamp('updated_at').notNull(),
|
|
19
|
+
ipAddress: text('ip_address'),
|
|
20
|
+
userAgent: text('user_agent'),
|
|
21
|
+
userId: text('user_id')
|
|
22
|
+
.notNull()
|
|
23
|
+
.references(() => user.id, { onDelete: 'cascade' }),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const account = pgTable('account', {
|
|
27
|
+
id: text('id').primaryKey(),
|
|
28
|
+
accountId: text('account_id').notNull(),
|
|
29
|
+
providerId: text('provider_id').notNull(),
|
|
30
|
+
userId: text('user_id')
|
|
31
|
+
.notNull()
|
|
32
|
+
.references(() => user.id, { onDelete: 'cascade' }),
|
|
33
|
+
accessToken: text('access_token'),
|
|
34
|
+
refreshToken: text('refresh_token'),
|
|
35
|
+
idToken: text('id_token'),
|
|
36
|
+
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
|
37
|
+
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
|
38
|
+
scope: text('scope'),
|
|
39
|
+
password: text('password'),
|
|
40
|
+
createdAt: timestamp('created_at').notNull(),
|
|
41
|
+
updatedAt: timestamp('updated_at').notNull(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const verification = pgTable('verification', {
|
|
45
|
+
id: text('id').primaryKey(),
|
|
46
|
+
identifier: text('identifier').notNull(),
|
|
47
|
+
value: text('value').notNull(),
|
|
48
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
49
|
+
createdAt: timestamp('created_at'),
|
|
50
|
+
updatedAt: timestamp('updated_at'),
|
|
51
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# AGENTS.md - Items Module
|
|
2
|
+
|
|
3
|
+
> See root [AGENTS.md](../../AGENTS.md) for project overview
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
**Reference implementation** of a Kuckit module demonstrating the full pattern.
|
|
8
|
+
|
|
9
|
+
Use this module as a template when creating new modules.
|
|
10
|
+
|
|
11
|
+
## Structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
src/
|
|
15
|
+
├── domain/
|
|
16
|
+
│ └── item.entity.ts # Entity schema (Zod)
|
|
17
|
+
├── ports/
|
|
18
|
+
│ └── item.repository.ts # Repository interface
|
|
19
|
+
├── adapters/
|
|
20
|
+
│ └── item.drizzle.ts # Drizzle implementation
|
|
21
|
+
├── usecases/
|
|
22
|
+
│ ├── create-item.ts # Create item use case
|
|
23
|
+
│ ├── get-item.ts # Get single item
|
|
24
|
+
│ ├── list-items.ts # List all items
|
|
25
|
+
│ └── delete-item.ts # Delete item
|
|
26
|
+
├── api/
|
|
27
|
+
│ └── items.router.ts # oRPC router
|
|
28
|
+
├── ui/
|
|
29
|
+
│ └── ItemsPage.tsx # React component
|
|
30
|
+
├── module.ts # Server module definition
|
|
31
|
+
├── client-module.ts # Client module definition
|
|
32
|
+
└── index.ts # Public exports
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Key Patterns
|
|
36
|
+
|
|
37
|
+
### Domain Layer
|
|
38
|
+
|
|
39
|
+
Pure Zod schemas, no dependencies:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export const ItemSchema = z.object({
|
|
43
|
+
id: z.string().uuid(),
|
|
44
|
+
name: z.string().min(1),
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Ports Layer
|
|
49
|
+
|
|
50
|
+
Interfaces only, depend on domain:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export interface ItemRepository {
|
|
54
|
+
findAll(): Promise<Item[]>
|
|
55
|
+
findById(id: string): Promise<Item | null>
|
|
56
|
+
create(item: CreateItem): Promise<Item>
|
|
57
|
+
delete(id: string): Promise<void>
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Adapters Layer
|
|
62
|
+
|
|
63
|
+
Implement ports using Drizzle:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
export const makeItemRepository = (db: Database): ItemRepository => ({
|
|
67
|
+
findAll: () => db.select().from(itemsTable),
|
|
68
|
+
// ...
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Use Cases
|
|
73
|
+
|
|
74
|
+
Business logic, depend on ports:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
export const createItem =
|
|
78
|
+
(repo: ItemRepository) =>
|
|
79
|
+
async (input: CreateItem): Promise<Item> => {
|
|
80
|
+
return repo.create(input)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Module Definition
|
|
85
|
+
|
|
86
|
+
Lifecycle hooks for registration:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
export const kuckitModule = defineKuckitModule({
|
|
90
|
+
id: 'items',
|
|
91
|
+
register(ctx) {
|
|
92
|
+
/* DI registration */
|
|
93
|
+
},
|
|
94
|
+
registerApi(ctx) {
|
|
95
|
+
/* API routes */
|
|
96
|
+
},
|
|
97
|
+
onBootstrap(ctx) {
|
|
98
|
+
/* Startup logic */
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Creating a New Module
|
|
104
|
+
|
|
105
|
+
1. Copy this entire `items-module` folder
|
|
106
|
+
2. Rename to `your-module`
|
|
107
|
+
3. Update `package.json` name
|
|
108
|
+
4. Replace domain entities
|
|
109
|
+
5. Update ports and adapters
|
|
110
|
+
6. Implement use cases
|
|
111
|
+
7. Create API router
|
|
112
|
+
8. Register in server's `config/modules.ts`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__APP_NAME_KEBAB__/items-module",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./src/client-module.ts",
|
|
15
|
+
"default": "./src/client-module.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"typescript": "^5"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@kuckit/sdk": "^1.0.0",
|
|
23
|
+
"@kuckit/sdk-react": "^1.0.0",
|
|
24
|
+
"@orpc/server": "^1.10.0",
|
|
25
|
+
"@orpc/zod": "^1.10.0",
|
|
26
|
+
"drizzle-orm": "^0.44.2",
|
|
27
|
+
"zod": "^4.1.11",
|
|
28
|
+
"react": "^19.1.0",
|
|
29
|
+
"@__APP_NAME_KEBAB__/api": "workspace:*",
|
|
30
|
+
"@__APP_NAME_KEBAB__/db": "workspace:*"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'
|
|
2
|
+
import { eq } from 'drizzle-orm'
|
|
3
|
+
import type { ItemRepository } from '../ports/item.repository'
|
|
4
|
+
import type { Item, CreateItemInput, UpdateItemInput } from '../domain/item.entity'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Items table schema for Drizzle ORM
|
|
8
|
+
*/
|
|
9
|
+
export const itemsTable = pgTable('items', {
|
|
10
|
+
id: text('id').primaryKey(),
|
|
11
|
+
name: text('name').notNull(),
|
|
12
|
+
description: text('description'),
|
|
13
|
+
createdAt: timestamp('created_at').notNull().defaultNow(),
|
|
14
|
+
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
|
15
|
+
userId: text('user_id').notNull(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a Drizzle-based item repository
|
|
20
|
+
*/
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
export function makeItemRepository(db: any): ItemRepository {
|
|
23
|
+
return {
|
|
24
|
+
async findById(id: string): Promise<Item | null> {
|
|
25
|
+
const results = await db.select().from(itemsTable).where(eq(itemsTable.id, id))
|
|
26
|
+
return results[0] ?? null
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async findByUserId(userId: string): Promise<Item[]> {
|
|
30
|
+
return db.select().from(itemsTable).where(eq(itemsTable.userId, userId))
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async create(input: CreateItemInput & { id: string; userId: string }): Promise<Item> {
|
|
34
|
+
const now = new Date()
|
|
35
|
+
const item = {
|
|
36
|
+
id: input.id,
|
|
37
|
+
name: input.name,
|
|
38
|
+
description: input.description ?? null,
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
userId: input.userId,
|
|
42
|
+
}
|
|
43
|
+
await db.insert(itemsTable).values(item)
|
|
44
|
+
return item as Item
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async update(input: UpdateItemInput): Promise<Item | null> {
|
|
48
|
+
const existing = await this.findById(input.id)
|
|
49
|
+
if (!existing) return null
|
|
50
|
+
|
|
51
|
+
const updated = {
|
|
52
|
+
...existing,
|
|
53
|
+
...(input.name !== undefined && { name: input.name }),
|
|
54
|
+
...(input.description !== undefined && { description: input.description }),
|
|
55
|
+
updatedAt: new Date(),
|
|
56
|
+
}
|
|
57
|
+
await db.update(itemsTable).set(updated).where(eq(itemsTable.id, input.id))
|
|
58
|
+
return updated
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async delete(id: string): Promise<boolean> {
|
|
62
|
+
const result = await db.delete(itemsTable).where(eq(itemsTable.id, id))
|
|
63
|
+
return result.rowCount > 0
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { protectedProcedure } from '@__APP_NAME_KEBAB__/api'
|
|
3
|
+
import { CreateItemInputSchema } from '../domain/item.entity'
|
|
4
|
+
import type { ItemRepository } from '../ports/item.repository'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Items oRPC router
|
|
8
|
+
* Provides CRUD operations for items
|
|
9
|
+
*/
|
|
10
|
+
export const itemsRouter = {
|
|
11
|
+
list: protectedProcedure.input(z.object({})).handler(async ({ context }) => {
|
|
12
|
+
const userId = context.session?.user?.id
|
|
13
|
+
if (!userId) throw new Error('User not authenticated')
|
|
14
|
+
|
|
15
|
+
// In a real app, you'd resolve this from the DI container
|
|
16
|
+
const items = await context.di.resolve<ItemRepository>('itemRepository').findByUserId(userId)
|
|
17
|
+
return items
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
get: protectedProcedure
|
|
21
|
+
.input(z.object({ id: z.string() }))
|
|
22
|
+
.handler(async ({ input, context }) => {
|
|
23
|
+
const item = await context.di.resolve<ItemRepository>('itemRepository').findById(input.id)
|
|
24
|
+
return item
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
create: protectedProcedure.input(CreateItemInputSchema).handler(async ({ input, context }) => {
|
|
28
|
+
const userId = context.session?.user?.id
|
|
29
|
+
if (!userId) throw new Error('User not authenticated')
|
|
30
|
+
|
|
31
|
+
const id = crypto.randomUUID()
|
|
32
|
+
const item = await context.di.resolve<ItemRepository>('itemRepository').create({
|
|
33
|
+
id,
|
|
34
|
+
name: input.name,
|
|
35
|
+
description: input.description,
|
|
36
|
+
userId,
|
|
37
|
+
})
|
|
38
|
+
return item
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
delete: protectedProcedure
|
|
42
|
+
.input(z.object({ id: z.string() }))
|
|
43
|
+
.handler(async ({ input, context }) => {
|
|
44
|
+
const success = await context.di.resolve<ItemRepository>('itemRepository').delete(input.id)
|
|
45
|
+
return { success }
|
|
46
|
+
}),
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineKuckitClientModule, type KuckitClientModuleContext } from '@kuckit/sdk-react'
|
|
2
|
+
import { ItemsPage } from './ui/ItemsPage'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Items client module
|
|
6
|
+
* Registers routes and components for the web app
|
|
7
|
+
*/
|
|
8
|
+
export const kuckitClientModule = defineKuckitClientModule({
|
|
9
|
+
id: 'items',
|
|
10
|
+
displayName: 'Items',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
|
|
13
|
+
register(ctx: KuckitClientModuleContext) {
|
|
14
|
+
// Register the items page component
|
|
15
|
+
ctx.registerComponent('ItemsPage', ItemsPage)
|
|
16
|
+
|
|
17
|
+
// Add route for items page
|
|
18
|
+
ctx.addRoute({
|
|
19
|
+
id: 'items',
|
|
20
|
+
path: '/items',
|
|
21
|
+
component: ItemsPage,
|
|
22
|
+
meta: {
|
|
23
|
+
title: 'Items',
|
|
24
|
+
requiresAuth: true,
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Add navigation item
|
|
29
|
+
ctx.addNavItem({
|
|
30
|
+
id: 'items-nav',
|
|
31
|
+
label: 'Items',
|
|
32
|
+
path: '/items',
|
|
33
|
+
icon: 'list',
|
|
34
|
+
order: 10,
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export { ItemsPage } from './ui/ItemsPage'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Item entity schema
|
|
5
|
+
*/
|
|
6
|
+
export const ItemSchema = z.object({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
name: z.string().min(1, 'Name is required'),
|
|
9
|
+
description: z.string().optional(),
|
|
10
|
+
createdAt: z.date(),
|
|
11
|
+
updatedAt: z.date(),
|
|
12
|
+
userId: z.string(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type Item = z.infer<typeof ItemSchema>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create item input schema
|
|
19
|
+
*/
|
|
20
|
+
export const CreateItemInputSchema = z.object({
|
|
21
|
+
name: z.string().min(1, 'Name is required'),
|
|
22
|
+
description: z.string().optional(),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export type CreateItemInput = z.infer<typeof CreateItemInputSchema>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Update item input schema
|
|
29
|
+
*/
|
|
30
|
+
export const UpdateItemInputSchema = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
name: z.string().min(1, 'Name is required').optional(),
|
|
33
|
+
description: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export type UpdateItemInput = z.infer<typeof UpdateItemInputSchema>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Server module exports
|
|
2
|
+
export { kuckitModule } from './module'
|
|
3
|
+
export type { ItemsModuleConfig } from './module'
|
|
4
|
+
|
|
5
|
+
// Domain exports
|
|
6
|
+
export * from './domain/item.entity'
|
|
7
|
+
|
|
8
|
+
// Port exports
|
|
9
|
+
export type { ItemRepository } from './ports/item.repository'
|
|
10
|
+
|
|
11
|
+
// Use case exports
|
|
12
|
+
export { makeListItems } from './usecases/list-items'
|
|
13
|
+
export { makeCreateItem } from './usecases/create-item'
|
|
14
|
+
export { makeGetItem } from './usecases/get-item'
|
|
15
|
+
export { makeDeleteItem } from './usecases/delete-item'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineKuckitModule, asFunction, type KuckitModuleContext } from '@kuckit/sdk'
|
|
2
|
+
import { itemsTable, makeItemRepository } from './adapters/item.drizzle'
|
|
3
|
+
import { itemsRouter } from './api/items.router'
|
|
4
|
+
|
|
5
|
+
export type ItemsModuleConfig = Record<string, never>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Items module - example Kuckit module demonstrating the full pattern
|
|
9
|
+
*
|
|
10
|
+
* This module shows:
|
|
11
|
+
* - Domain entity with Zod validation
|
|
12
|
+
* - Repository port/adapter pattern
|
|
13
|
+
* - Use cases for business logic
|
|
14
|
+
* - oRPC router for API endpoints
|
|
15
|
+
*/
|
|
16
|
+
export const kuckitModule = defineKuckitModule<ItemsModuleConfig>({
|
|
17
|
+
id: 'items',
|
|
18
|
+
displayName: 'Items',
|
|
19
|
+
description: 'Example items module for CRUD operations',
|
|
20
|
+
version: '0.1.0',
|
|
21
|
+
|
|
22
|
+
async register(ctx: KuckitModuleContext<ItemsModuleConfig>) {
|
|
23
|
+
const { container } = ctx
|
|
24
|
+
|
|
25
|
+
// Register schema for migrations
|
|
26
|
+
ctx.registerSchema('items', itemsTable)
|
|
27
|
+
|
|
28
|
+
// Register repository
|
|
29
|
+
container.register({
|
|
30
|
+
itemRepository: asFunction(({ db }) => makeItemRepository(db)).scoped(),
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
registerApi(ctx) {
|
|
35
|
+
ctx.addApiRegistration({
|
|
36
|
+
type: 'rpc-router',
|
|
37
|
+
name: 'items',
|
|
38
|
+
router: itemsRouter,
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async onBootstrap(ctx: KuckitModuleContext<ItemsModuleConfig>) {
|
|
43
|
+
const { container } = ctx
|
|
44
|
+
const logger = container.resolve('logger')
|
|
45
|
+
logger.info('Items module initialized')
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async onShutdown(ctx: KuckitModuleContext<ItemsModuleConfig>) {
|
|
49
|
+
const { container } = ctx
|
|
50
|
+
const logger = container.resolve('logger')
|
|
51
|
+
logger.info('Items module shutting down')
|
|
52
|
+
},
|
|
53
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Item, CreateItemInput, UpdateItemInput } from '../domain/item.entity'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Item repository port interface
|
|
5
|
+
* Defines the contract for item persistence operations
|
|
6
|
+
*/
|
|
7
|
+
export interface ItemRepository {
|
|
8
|
+
findById(id: string): Promise<Item | null>
|
|
9
|
+
findByUserId(userId: string): Promise<Item[]>
|
|
10
|
+
create(input: CreateItemInput & { id: string; userId: string }): Promise<Item>
|
|
11
|
+
update(input: UpdateItemInput): Promise<Item | null>
|
|
12
|
+
delete(id: string): Promise<boolean>
|
|
13
|
+
}
|