@spfn/cli 0.0.9 → 0.1.0-alpha.2
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 +283 -0
- package/bin/spfn.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +857 -0
- package/dist/templates/.guide/api-routes.md +388 -0
- package/dist/templates/server/entities/README.md +131 -0
- package/dist/templates/server/routes/examples/contract.ts +101 -0
- package/dist/templates/server/routes/examples/index.ts +112 -0
- package/dist/templates/server/routes/health/contract.ts +13 -0
- package/dist/templates/server/routes/health/index.ts +23 -0
- package/dist/templates/server/routes/index/contract.ts +13 -0
- package/dist/templates/server/routes/index/index.ts +28 -0
- package/package.json +67 -47
- package/lib/index.js +0 -19
- package/lib/login.js +0 -206
@@ -0,0 +1,388 @@
|
|
1
|
+
# SPFN Best Practices
|
2
|
+
|
3
|
+
## Core Philosophy
|
4
|
+
|
5
|
+
### 1. **Shared Types Between Next.js ↔ Server**
|
6
|
+
|
7
|
+
The most important philosophy of SPFN is **sharing the same types between frontend and backend**.
|
8
|
+
|
9
|
+
**❌ Wrong Approach (Type Duplication):**
|
10
|
+
```typescript
|
11
|
+
// Backend: src/server/routes/users/index.ts
|
12
|
+
interface User { // ❌ Defined only in server
|
13
|
+
id: number;
|
14
|
+
name: string;
|
15
|
+
}
|
16
|
+
|
17
|
+
// Frontend: src/app/users/page.tsx
|
18
|
+
interface User { // ❌ Same type redefined
|
19
|
+
id: number;
|
20
|
+
name: string;
|
21
|
+
}
|
22
|
+
```
|
23
|
+
|
24
|
+
**✅ Correct Approach (Shared Types):**
|
25
|
+
```typescript
|
26
|
+
// Shared: src/types/user.ts
|
27
|
+
export interface User {
|
28
|
+
id: number;
|
29
|
+
name: string;
|
30
|
+
email: string;
|
31
|
+
createdAt: Date;
|
32
|
+
}
|
33
|
+
|
34
|
+
export interface CreateUserRequest {
|
35
|
+
name: string;
|
36
|
+
email: string;
|
37
|
+
}
|
38
|
+
|
39
|
+
// Backend: src/server/routes/users/index.ts
|
40
|
+
import type { User, CreateUserRequest } from '@/types/user';
|
41
|
+
|
42
|
+
export async function POST(c: RouteContext) {
|
43
|
+
const data = await c.data<CreateUserRequest>();
|
44
|
+
// ...
|
45
|
+
return c.json<User>(newUser);
|
46
|
+
}
|
47
|
+
|
48
|
+
// Frontend: src/app/users/page.tsx
|
49
|
+
import type { User } from '@/types/user';
|
50
|
+
import { api } from '@/lib/api'; // Auto-generated!
|
51
|
+
|
52
|
+
async function getUsers(): Promise<User[]> {
|
53
|
+
return api.users.getUsers(); // Type-safe & auto-generated
|
54
|
+
}
|
55
|
+
```
|
56
|
+
|
57
|
+
---
|
58
|
+
|
59
|
+
### 2. **RouteContext Usage (Hono Wrapping Strategy)**
|
60
|
+
|
61
|
+
SPFN **wraps** Hono's Context to provide a more convenient API, while keeping full access to Hono's original Context via `c.raw`.
|
62
|
+
|
63
|
+
#### ❌ Don't Use `c.req` Directly
|
64
|
+
```typescript
|
65
|
+
// This DOES NOT work in SPFN!
|
66
|
+
export async function POST(c: RouteContext) {
|
67
|
+
const body = await c.req.json(); // ❌ Error: 'req' doesn't exist on RouteContext
|
68
|
+
return c.json({ ... });
|
69
|
+
}
|
70
|
+
```
|
71
|
+
|
72
|
+
#### ✅ Use SPFN's Wrapped API
|
73
|
+
```typescript
|
74
|
+
import type { RouteContext } from '@spfn/core';
|
75
|
+
|
76
|
+
export async function POST(c: RouteContext) {
|
77
|
+
// ✅ Request Body (SPFN wrapper)
|
78
|
+
const body = await c.data<CreateUserRequest>();
|
79
|
+
|
80
|
+
// ✅ Path Parameters (SPFN wrapper)
|
81
|
+
const userId = c.params.id; // /users/:id
|
82
|
+
|
83
|
+
// ✅ Query Parameters (SPFN wrapper)
|
84
|
+
const page = c.query.page; // /users?page=1
|
85
|
+
|
86
|
+
// ✅ Pageable (SPFN-only feature, Spring Boot style)
|
87
|
+
const { filters, sort, pagination } = c.pageable;
|
88
|
+
|
89
|
+
// ✅ JSON Response (same as Hono)
|
90
|
+
return c.json<User>(result);
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
#### ✅ Access Original Hono Context via `c.raw`
|
95
|
+
When you need Hono's native features, use `c.raw`:
|
96
|
+
|
97
|
+
```typescript
|
98
|
+
export async function POST(c: RouteContext) {
|
99
|
+
// ✅ Headers (via c.raw)
|
100
|
+
const authHeader = c.raw.req.header('Authorization');
|
101
|
+
const contentType = c.raw.req.header('Content-Type');
|
102
|
+
|
103
|
+
// ✅ Cookies (via c.raw)
|
104
|
+
const sessionToken = c.raw.req.cookie('session');
|
105
|
+
|
106
|
+
// ✅ Set Response Headers (via c.raw)
|
107
|
+
c.raw.header('X-Custom-Header', 'value');
|
108
|
+
c.raw.header('Cache-Control', 'no-cache');
|
109
|
+
|
110
|
+
// ✅ Context Variables - Share data between middlewares (via c.raw)
|
111
|
+
const userId = c.raw.get('userId');
|
112
|
+
c.raw.set('requestId', crypto.randomUUID());
|
113
|
+
|
114
|
+
// ✅ File Upload (via c.raw)
|
115
|
+
const formData = await c.raw.req.formData();
|
116
|
+
const file = formData.get('file');
|
117
|
+
|
118
|
+
// ✅ Raw Request object (via c.raw)
|
119
|
+
const method = c.raw.req.method;
|
120
|
+
const url = c.raw.req.url;
|
121
|
+
|
122
|
+
// You can still use SPFN's wrapped methods
|
123
|
+
const body = await c.data<CreateUserRequest>();
|
124
|
+
|
125
|
+
return c.json({ success: true });
|
126
|
+
}
|
127
|
+
```
|
128
|
+
|
129
|
+
**Summary:**
|
130
|
+
- **SPFN wrappers**: `c.data()`, `c.params`, `c.query`, `c.pageable`, `c.json()`
|
131
|
+
- **Hono original**: Everything via `c.raw` (e.g., `c.raw.req.header()`, `c.raw.req.cookie()`)
|
132
|
+
- **Both work together**: Use SPFN for convenience, `c.raw` for advanced Hono features
|
133
|
+
|
134
|
+
---
|
135
|
+
|
136
|
+
### 3. **Auto-Generated API Client**
|
137
|
+
|
138
|
+
SPFN automatically generates type-safe API clients from your routes.
|
139
|
+
|
140
|
+
#### Setup
|
141
|
+
|
142
|
+
Add to `package.json`:
|
143
|
+
```json
|
144
|
+
{
|
145
|
+
"scripts": {
|
146
|
+
"generate": "tsx node_modules/@spfn/core/dist/scripts/watch-all.js",
|
147
|
+
"generate:api": "tsx node_modules/@spfn/core/dist/scripts/generate-api-client.js",
|
148
|
+
"generate:types": "tsx node_modules/@spfn/core/dist/scripts/generate-types.js"
|
149
|
+
}
|
150
|
+
}
|
151
|
+
```
|
152
|
+
|
153
|
+
#### Watch Mode (Recommended)
|
154
|
+
```bash
|
155
|
+
npm run generate
|
156
|
+
```
|
157
|
+
|
158
|
+
This watches for changes and auto-regenerates:
|
159
|
+
- **Routes change** → API client regenerates (`src/lib/api/`)
|
160
|
+
- **Entities change** → Types regenerate (after migration)
|
161
|
+
|
162
|
+
#### Manual Generation
|
163
|
+
```bash
|
164
|
+
npm run generate:api # Generate API client
|
165
|
+
npm run generate:types # Generate types from entities
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Generated API Client Usage
|
169
|
+
|
170
|
+
**Backend Route:**
|
171
|
+
```typescript
|
172
|
+
// src/server/routes/users/index.ts
|
173
|
+
import type { User, CreateUserRequest } from '@/types/user';
|
174
|
+
|
175
|
+
export const meta = { tags: ['users'] };
|
176
|
+
|
177
|
+
export async function GET(c: RouteContext) {
|
178
|
+
const users = await db.select().from(usersTable);
|
179
|
+
return c.json<User[]>(users);
|
180
|
+
}
|
181
|
+
|
182
|
+
export async function POST(c: RouteContext) {
|
183
|
+
const data = await c.data<CreateUserRequest>();
|
184
|
+
const [user] = await db.insert(usersTable).values(data).returning();
|
185
|
+
return c.json<User>(user);
|
186
|
+
}
|
187
|
+
```
|
188
|
+
|
189
|
+
**Auto-Generated Client:**
|
190
|
+
```typescript
|
191
|
+
// src/lib/api/users.ts (auto-generated)
|
192
|
+
export const users = {
|
193
|
+
getUsers: () => request<User[]>('/users'),
|
194
|
+
createUsers: (data: CreateUserRequest) => request<User>('/users', 'POST', data),
|
195
|
+
};
|
196
|
+
```
|
197
|
+
|
198
|
+
**Frontend Usage:**
|
199
|
+
```typescript
|
200
|
+
// src/app/users/page.tsx
|
201
|
+
import { api } from '@/lib/api';
|
202
|
+
import type { User } from '@/types/user';
|
203
|
+
|
204
|
+
export default async function UsersPage() {
|
205
|
+
// ✅ Type-safe, auto-completed
|
206
|
+
const users = await api.users.getUsers();
|
207
|
+
|
208
|
+
return (
|
209
|
+
<ul>
|
210
|
+
{users.map(user => (
|
211
|
+
<li key={user.id}>{user.name}</li>
|
212
|
+
))}
|
213
|
+
</ul>
|
214
|
+
);
|
215
|
+
}
|
216
|
+
```
|
217
|
+
|
218
|
+
---
|
219
|
+
|
220
|
+
### 4. **Recommended Project Structure**
|
221
|
+
|
222
|
+
```
|
223
|
+
your-nextjs-project/
|
224
|
+
├── src/
|
225
|
+
│ ├── app/ # Next.js App Router (Frontend)
|
226
|
+
│ │ ├── users/
|
227
|
+
│ │ │ └── page.tsx
|
228
|
+
│ │ └── layout.tsx
|
229
|
+
│ │
|
230
|
+
│ ├── types/ # 🔥 Shared Types (Frontend ↔ Backend)
|
231
|
+
│ │ ├── user.ts # User-related types
|
232
|
+
│ │ ├── post.ts # Post-related types
|
233
|
+
│ │ └── api.ts # API Request/Response types
|
234
|
+
│ │
|
235
|
+
│ ├── lib/ # Frontend utilities
|
236
|
+
│ │ └── api/ # ⚡ Auto-generated API clients
|
237
|
+
│ │ ├── index.ts # Aggregated exports
|
238
|
+
│ │ ├── users.ts # User API client
|
239
|
+
│ │ └── posts.ts # Post API client
|
240
|
+
│ │
|
241
|
+
│ └── server/ # SPFN Backend
|
242
|
+
│ ├── routes/ # API Routes
|
243
|
+
│ │ ├── users/
|
244
|
+
│ │ │ ├── index.ts # GET /users, POST /users
|
245
|
+
│ │ │ └── [id].ts # GET /users/:id
|
246
|
+
│ │ └── posts/
|
247
|
+
│ │ └── index.ts
|
248
|
+
│ │
|
249
|
+
│ ├── entities/ # Drizzle Entities
|
250
|
+
│ │ └── users.ts
|
251
|
+
│ │
|
252
|
+
│ ├── middleware/ # Custom Middleware (optional)
|
253
|
+
│ │ └── auth.ts
|
254
|
+
│ │
|
255
|
+
│ └── server.config.ts # Server Config (optional)
|
256
|
+
│
|
257
|
+
└── package.json
|
258
|
+
```
|
259
|
+
|
260
|
+
---
|
261
|
+
|
262
|
+
### 5. **Entities vs Types**
|
263
|
+
|
264
|
+
#### Entity = Database Schema
|
265
|
+
```typescript
|
266
|
+
// src/server/entities/users.ts
|
267
|
+
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';
|
268
|
+
|
269
|
+
export const users = pgTable('users', {
|
270
|
+
id: serial('id').primaryKey(),
|
271
|
+
name: varchar('name', { length: 100 }).notNull(),
|
272
|
+
email: varchar('email', { length: 255 }).notNull().unique(),
|
273
|
+
createdAt: timestamp('created_at').defaultNow(),
|
274
|
+
});
|
275
|
+
```
|
276
|
+
|
277
|
+
#### Type = API Contract (Shared with Frontend)
|
278
|
+
```typescript
|
279
|
+
// src/types/user.ts (auto-generated or manual)
|
280
|
+
export interface User {
|
281
|
+
id: number;
|
282
|
+
name: string;
|
283
|
+
email: string;
|
284
|
+
createdAt: Date;
|
285
|
+
}
|
286
|
+
|
287
|
+
export interface CreateUserRequest {
|
288
|
+
name: string;
|
289
|
+
email: string;
|
290
|
+
}
|
291
|
+
```
|
292
|
+
|
293
|
+
---
|
294
|
+
|
295
|
+
### 6. **TypeScript Path Aliases**
|
296
|
+
|
297
|
+
Simplify imports with `tsconfig.json`:
|
298
|
+
|
299
|
+
```json
|
300
|
+
{
|
301
|
+
"compilerOptions": {
|
302
|
+
"paths": {
|
303
|
+
"@/*": ["./src/*"],
|
304
|
+
"@/types/*": ["./src/types/*"],
|
305
|
+
"@/server/*": ["./src/server/*"],
|
306
|
+
"@/lib/*": ["./src/lib/*"]
|
307
|
+
}
|
308
|
+
}
|
309
|
+
}
|
310
|
+
```
|
311
|
+
|
312
|
+
Usage:
|
313
|
+
```typescript
|
314
|
+
// ❌ Relative paths (avoid)
|
315
|
+
import { User } from '../../../types/user';
|
316
|
+
import { users } from '../../entities/users';
|
317
|
+
|
318
|
+
// ✅ Use aliases
|
319
|
+
import type { User } from '@/types/user';
|
320
|
+
import { users } from '@/server/entities/users';
|
321
|
+
import { api } from '@/lib/api';
|
322
|
+
```
|
323
|
+
|
324
|
+
---
|
325
|
+
|
326
|
+
## Quick Reference
|
327
|
+
|
328
|
+
### RouteContext API Comparison
|
329
|
+
|
330
|
+
| Feature | SPFN Wrapper | Hono Original (via c.raw) |
|
331
|
+
|---------|--------------|---------------------------|
|
332
|
+
| Request Body | `c.data<T>()` | `c.raw.req.json()` |
|
333
|
+
| Path Params | `c.params.id` | `c.raw.req.param('id')` |
|
334
|
+
| Query Params | `c.query.page` | `c.raw.req.query('page')` |
|
335
|
+
| Headers | `c.raw.req.header('...')` | ✅ |
|
336
|
+
| Cookies | `c.raw.req.cookie('...')` | ✅ |
|
337
|
+
| Response | `c.json()` | `c.raw.json()` |
|
338
|
+
| Pagination | `c.pageable` | ❌ (SPFN-only) |
|
339
|
+
| Context Variables | `c.raw.get()` / `c.raw.set()` | ✅ |
|
340
|
+
| Raw Request | `c.raw.req` | ✅ |
|
341
|
+
|
342
|
+
### Why SPFN Wraps Hono Context
|
343
|
+
|
344
|
+
**Benefits:**
|
345
|
+
- 🎯 Simpler API for common operations
|
346
|
+
- 🔒 Type-safe request parsing with `c.data<T>()`
|
347
|
+
- 📊 Built-in pagination support (`c.pageable`)
|
348
|
+
- 🚀 Spring Boot-inspired developer experience
|
349
|
+
- 🔓 **Full Hono access via `c.raw`** - No limitations!
|
350
|
+
|
351
|
+
**Example - Combining Both:**
|
352
|
+
```typescript
|
353
|
+
export async function POST(c: RouteContext) {
|
354
|
+
// SPFN wrapper (simple & type-safe)
|
355
|
+
const body = await c.data<CreateUserRequest>();
|
356
|
+
const userId = c.params.id;
|
357
|
+
|
358
|
+
// Hono original (advanced features)
|
359
|
+
const authToken = c.raw.req.header('Authorization');
|
360
|
+
c.raw.set('userId', userId);
|
361
|
+
|
362
|
+
return c.json({ success: true });
|
363
|
+
}
|
364
|
+
```
|
365
|
+
|
366
|
+
---
|
367
|
+
|
368
|
+
### Auto-Generated API Client Commands
|
369
|
+
|
370
|
+
```bash
|
371
|
+
# Start watch mode (recommended for development)
|
372
|
+
npm run generate
|
373
|
+
|
374
|
+
# Manually generate API client from routes
|
375
|
+
npm run generate:api
|
376
|
+
|
377
|
+
# Manually generate types from entities
|
378
|
+
npm run generate:types
|
379
|
+
```
|
380
|
+
|
381
|
+
---
|
382
|
+
|
383
|
+
## Learn More
|
384
|
+
|
385
|
+
- [SPFN Core Documentation](https://spfn.dev/docs)
|
386
|
+
- [Hono Documentation](https://hono.dev) - Full Hono API available via `c.raw`
|
387
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
388
|
+
- [Drizzle ORM](https://orm.drizzle.team/)
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# Entities
|
2
|
+
|
3
|
+
Define your Drizzle ORM entities here. These are your database table schemas.
|
4
|
+
|
5
|
+
Use `spfn generate <entity-name>` to scaffold a new entity with CRUD routes.
|
6
|
+
|
7
|
+
## Entity Helper Functions
|
8
|
+
|
9
|
+
SPFN provides helper functions to reduce boilerplate when defining entities:
|
10
|
+
|
11
|
+
```typescript
|
12
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
13
|
+
import { id, timestamps, foreignKey } from '@spfn/core';
|
14
|
+
|
15
|
+
// Simple entity with helpers
|
16
|
+
export const users = pgTable('users', {
|
17
|
+
id: id(), // bigserial primary key
|
18
|
+
email: text('email').unique(),
|
19
|
+
...timestamps(), // createdAt + updatedAt
|
20
|
+
});
|
21
|
+
|
22
|
+
// Entity with foreign key
|
23
|
+
export const posts = pgTable('posts', {
|
24
|
+
id: id(),
|
25
|
+
title: text('title').notNull(),
|
26
|
+
authorId: foreignKey('author', () => users.id), // Cascade delete
|
27
|
+
...timestamps(),
|
28
|
+
});
|
29
|
+
|
30
|
+
export type User = typeof users.$inferSelect;
|
31
|
+
export type Post = typeof posts.$inferSelect;
|
32
|
+
```
|
33
|
+
|
34
|
+
**Available helpers:**
|
35
|
+
- `id()` - Auto-incrementing bigserial primary key
|
36
|
+
- `timestamps()` - Adds `createdAt` and `updatedAt` fields
|
37
|
+
- `foreignKey(name, ref)` - Foreign key with cascade delete
|
38
|
+
- `optionalForeignKey(name, ref)` - Nullable foreign key
|
39
|
+
|
40
|
+
## Pattern 1: Simple Route (No Repository)
|
41
|
+
|
42
|
+
Use `getDb()` directly in routes for simple queries:
|
43
|
+
|
44
|
+
```typescript
|
45
|
+
// src/server/routes/users/index.ts
|
46
|
+
import type { RouteContext } from '@spfn/core';
|
47
|
+
import { getDb } from '@spfn/core';
|
48
|
+
import { users } from '../../entities/users.js';
|
49
|
+
|
50
|
+
export async function GET(c: RouteContext)
|
51
|
+
{
|
52
|
+
const db = getDb();
|
53
|
+
const allUsers = await db.select().from(users);
|
54
|
+
return c.json(allUsers);
|
55
|
+
}
|
56
|
+
```
|
57
|
+
|
58
|
+
## Pattern 2: Repository Pattern (Recommended)
|
59
|
+
|
60
|
+
For complex business logic, use the Repository pattern:
|
61
|
+
|
62
|
+
**1. Create a repository:**
|
63
|
+
|
64
|
+
```typescript
|
65
|
+
// src/server/repositories/user.repository.ts
|
66
|
+
import { eq } from 'drizzle-orm';
|
67
|
+
import { BaseRepository } from '@spfn/core';
|
68
|
+
import { users, type User, type NewUser } from '../entities/users.js';
|
69
|
+
|
70
|
+
export class UserRepository extends BaseRepository<typeof users, User, NewUser>
|
71
|
+
{
|
72
|
+
constructor()
|
73
|
+
{
|
74
|
+
super(users);
|
75
|
+
}
|
76
|
+
|
77
|
+
async findByEmail(email: string): Promise<User | undefined>
|
78
|
+
{
|
79
|
+
const db = await this.getDb();
|
80
|
+
const result = await db.select().from(users).where(eq(users.email, email));
|
81
|
+
return result[0];
|
82
|
+
}
|
83
|
+
}
|
84
|
+
```
|
85
|
+
|
86
|
+
**2. Use in routes with automatic transactions:**
|
87
|
+
|
88
|
+
```typescript
|
89
|
+
// src/server/routes/users/POST.ts
|
90
|
+
import type { RouteContext } from '@spfn/core';
|
91
|
+
import { Transactional } from '@spfn/core';
|
92
|
+
import { UserRepository } from '../../repositories/user.repository.js';
|
93
|
+
|
94
|
+
const userRepo = new UserRepository();
|
95
|
+
|
96
|
+
// POST /users - Transaction automatically managed by Transactional() middleware
|
97
|
+
export const POST = [
|
98
|
+
Transactional(),
|
99
|
+
async (c: RouteContext) =>
|
100
|
+
{
|
101
|
+
const data = await c.req.json();
|
102
|
+
const user = await userRepo.create(data);
|
103
|
+
return c.json(user, 201);
|
104
|
+
}
|
105
|
+
];
|
106
|
+
```
|
107
|
+
|
108
|
+
## @spfn/core Features
|
109
|
+
|
110
|
+
- **Zero-Config**: Framework auto-loads routes from `routes/` directory
|
111
|
+
- **File-based Routing**: Next.js App Router style (GET.ts, POST.ts, [id].ts, etc.)
|
112
|
+
- **Repository Pattern**: Spring Data JPA style with `BaseRepository`
|
113
|
+
- **Automatic Transactions**: `Transactional()` middleware with AsyncLocalStorage
|
114
|
+
- **Type-safe DB Access**: `getDb()` automatically uses transaction context when available
|
115
|
+
|
116
|
+
## Database Migration
|
117
|
+
|
118
|
+
```bash
|
119
|
+
# Generate migration from your entities
|
120
|
+
npx drizzle-kit generate:pg
|
121
|
+
|
122
|
+
# Run migrations
|
123
|
+
npx drizzle-kit push:pg
|
124
|
+
```
|
125
|
+
|
126
|
+
## Learn More
|
127
|
+
|
128
|
+
- [Getting Started](https://spfn.dev/docs/getting-started)
|
129
|
+
- [Routing Guide](https://spfn.dev/docs/routing)
|
130
|
+
- [Repository Pattern](https://spfn.dev/docs/repository)
|
131
|
+
- [Transaction Management](https://spfn.dev/docs/transactions)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Example Contracts
|
5
|
+
*
|
6
|
+
* Demonstrates various contract patterns
|
7
|
+
*/
|
8
|
+
|
9
|
+
/**
|
10
|
+
* GET /examples - List examples
|
11
|
+
*/
|
12
|
+
export const getExamplesContract = {
|
13
|
+
method: 'GET' as const,
|
14
|
+
path: '/',
|
15
|
+
query: Type.Object({
|
16
|
+
limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
|
17
|
+
offset: Type.Optional(Type.Number({ minimum: 0 }))
|
18
|
+
}),
|
19
|
+
response: Type.Object({
|
20
|
+
examples: Type.Array(Type.Object({
|
21
|
+
id: Type.String(),
|
22
|
+
name: Type.String(),
|
23
|
+
description: Type.String()
|
24
|
+
})),
|
25
|
+
total: Type.Number(),
|
26
|
+
limit: Type.Number(),
|
27
|
+
offset: Type.Number()
|
28
|
+
})
|
29
|
+
};
|
30
|
+
|
31
|
+
/**
|
32
|
+
* GET /examples/:id - Get single example
|
33
|
+
*/
|
34
|
+
export const getExampleContract = {
|
35
|
+
method: 'GET' as const,
|
36
|
+
path: '/:id',
|
37
|
+
params: Type.Object({
|
38
|
+
id: Type.String()
|
39
|
+
}),
|
40
|
+
response: Type.Object({
|
41
|
+
id: Type.String(),
|
42
|
+
name: Type.String(),
|
43
|
+
description: Type.String(),
|
44
|
+
createdAt: Type.Number(),
|
45
|
+
updatedAt: Type.Number()
|
46
|
+
})
|
47
|
+
};
|
48
|
+
|
49
|
+
/**
|
50
|
+
* POST /examples - Create example
|
51
|
+
*/
|
52
|
+
export const createExampleContract = {
|
53
|
+
method: 'POST' as const,
|
54
|
+
path: '/',
|
55
|
+
body: Type.Object({
|
56
|
+
name: Type.String(),
|
57
|
+
description: Type.String()
|
58
|
+
}),
|
59
|
+
response: Type.Object({
|
60
|
+
id: Type.String(),
|
61
|
+
name: Type.String(),
|
62
|
+
description: Type.String(),
|
63
|
+
createdAt: Type.Number()
|
64
|
+
})
|
65
|
+
};
|
66
|
+
|
67
|
+
/**
|
68
|
+
* PUT /examples/:id - Update example
|
69
|
+
*/
|
70
|
+
export const updateExampleContract = {
|
71
|
+
method: 'PUT' as const,
|
72
|
+
path: '/:id',
|
73
|
+
params: Type.Object({
|
74
|
+
id: Type.String()
|
75
|
+
}),
|
76
|
+
body: Type.Object({
|
77
|
+
name: Type.Optional(Type.String()),
|
78
|
+
description: Type.Optional(Type.String())
|
79
|
+
}),
|
80
|
+
response: Type.Object({
|
81
|
+
id: Type.String(),
|
82
|
+
name: Type.String(),
|
83
|
+
description: Type.String(),
|
84
|
+
updatedAt: Type.Number()
|
85
|
+
})
|
86
|
+
};
|
87
|
+
|
88
|
+
/**
|
89
|
+
* DELETE /examples/:id - Delete example
|
90
|
+
*/
|
91
|
+
export const deleteExampleContract = {
|
92
|
+
method: 'DELETE' as const,
|
93
|
+
path: '/:id',
|
94
|
+
params: Type.Object({
|
95
|
+
id: Type.String()
|
96
|
+
}),
|
97
|
+
response: Type.Object({
|
98
|
+
success: Type.Boolean(),
|
99
|
+
id: Type.String()
|
100
|
+
})
|
101
|
+
};
|