@martinffx/atelier 2.0.0 → 3.0.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/CHANGELOG.md +14 -0
- package/README.md +94 -253
- package/package.json +1 -1
- package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +6 -6
- package/skills/spec-brainstorm/SKILL.md +5 -5
- package/skills/spec-implement/SKILL.md +2 -3
- package/skills/spec-plan/SKILL.md +21 -29
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
# Fastify Plugin Architecture
|
|
2
|
-
|
|
3
|
-
Fastify plugins enable modular, testable dependency injection using the `fastify-plugin` wrapper.
|
|
4
|
-
|
|
5
|
-
## Core Concept
|
|
6
|
-
|
|
7
|
-
Plugins:
|
|
8
|
-
- Encapsulate related functionality (repos, services, auth, etc.)
|
|
9
|
-
- Use decorators to add properties to the Fastify instance
|
|
10
|
-
- Enable dependency injection through plugin registration order
|
|
11
|
-
- Support plugin-level scoping and encapsulation
|
|
12
|
-
|
|
13
|
-
## Basic Plugin
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import fp from 'fastify-plugin'
|
|
17
|
-
import type { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
18
|
-
|
|
19
|
-
// Define what the plugin adds to FastifyInstance
|
|
20
|
-
declare module 'fastify' {
|
|
21
|
-
interface FastifyInstance {
|
|
22
|
-
config: {
|
|
23
|
-
databaseUrl: string
|
|
24
|
-
jwtSecret: string
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const ConfigPlugin: FastifyPluginAsync = async (server: FastifyInstance) => {
|
|
30
|
-
server.decorate('config', {
|
|
31
|
-
databaseUrl: process.env.DATABASE_URL!,
|
|
32
|
-
jwtSecret: process.env.JWT_SECRET!,
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Wrap with fp() to prevent plugin encapsulation
|
|
37
|
-
export default fp(ConfigPlugin)
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Repository Plugin
|
|
41
|
-
|
|
42
|
-
Provide database access layer to the application:
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import fp from 'fastify-plugin'
|
|
46
|
-
import type { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
47
|
-
import { drizzle } from 'drizzle-orm/node-postgres'
|
|
48
|
-
import * as schema from './schema'
|
|
49
|
-
import { UserRepo } from './repo/UserRepo'
|
|
50
|
-
import { PostRepo } from './repo/PostRepo'
|
|
51
|
-
|
|
52
|
-
// Extend FastifyInstance
|
|
53
|
-
declare module 'fastify' {
|
|
54
|
-
interface FastifyInstance {
|
|
55
|
-
repo: Repos
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface Repos {
|
|
60
|
-
userRepo: UserRepo
|
|
61
|
-
postRepo: PostRepo
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface RepoPluginOptions {
|
|
65
|
-
db?: ReturnType<typeof drizzle>
|
|
66
|
-
repos?: Partial<Repos>
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const RepoPlugin: FastifyPluginAsync<RepoPluginOptions> = async (
|
|
70
|
-
server: FastifyInstance,
|
|
71
|
-
opts: RepoPluginOptions
|
|
72
|
-
) => {
|
|
73
|
-
// Use provided db or create from config
|
|
74
|
-
const db = opts.db ?? drizzle(server.config.databaseUrl, { schema })
|
|
75
|
-
|
|
76
|
-
// Use provided repos or create new instances
|
|
77
|
-
const repos: Repos = {
|
|
78
|
-
userRepo: opts.repos?.userRepo ?? new UserRepo(db),
|
|
79
|
-
postRepo: opts.repos?.postRepo ?? new PostRepo(db),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
server.decorate('repo', repos)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export default fp(RepoPlugin)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Service Plugin
|
|
89
|
-
|
|
90
|
-
Business logic layer that depends on repositories:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
import fp from 'fastify-plugin'
|
|
94
|
-
import type { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
95
|
-
import { UserService } from './services/UserService'
|
|
96
|
-
import { PostService } from './services/PostService'
|
|
97
|
-
|
|
98
|
-
declare module 'fastify' {
|
|
99
|
-
interface FastifyInstance {
|
|
100
|
-
services: Services
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
interface Services {
|
|
105
|
-
userService: UserService
|
|
106
|
-
postService: PostService
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
interface ServicePluginOptions {
|
|
110
|
-
services?: Partial<Services>
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const ServicePlugin: FastifyPluginAsync<ServicePluginOptions> = async (
|
|
114
|
-
server: FastifyInstance,
|
|
115
|
-
opts: ServicePluginOptions
|
|
116
|
-
) => {
|
|
117
|
-
// Services depend on repos (registered by RepoPlugin)
|
|
118
|
-
const services: Services = {
|
|
119
|
-
userService: opts.services?.userService ?? new UserService(server.repo.userRepo),
|
|
120
|
-
postService: opts.services?.postService ?? new PostService(server.repo.postRepo),
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
server.decorate('services', services)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export default fp(ServicePlugin)
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Router Plugin
|
|
130
|
-
|
|
131
|
-
HTTP routes that depend on services:
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
import type { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
135
|
-
import { userRoutes } from './routes/users'
|
|
136
|
-
import { postRoutes } from './routes/posts'
|
|
137
|
-
|
|
138
|
-
// Note: No fp() wrapper - routers should be encapsulated
|
|
139
|
-
const RouterPlugin: FastifyPluginAsync = async (server: FastifyInstance) => {
|
|
140
|
-
// Routes can access server.services.* and server.repo.*
|
|
141
|
-
await server.register(userRoutes, { prefix: '/users' })
|
|
142
|
-
await server.register(postRoutes, { prefix: '/posts' })
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export default RouterPlugin
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Auth Plugin
|
|
149
|
-
|
|
150
|
-
Authentication and authorization middleware:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
import fp from 'fastify-plugin'
|
|
154
|
-
import type { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'
|
|
155
|
-
import jwt from '@fastify/jwt'
|
|
156
|
-
|
|
157
|
-
declare module 'fastify' {
|
|
158
|
-
interface FastifyRequest {
|
|
159
|
-
token: {
|
|
160
|
-
userId: string
|
|
161
|
-
role: string
|
|
162
|
-
permissions: string[]
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
interface FastifyInstance {
|
|
167
|
-
hasPermissions: (required: string[]) => (
|
|
168
|
-
request: FastifyRequest,
|
|
169
|
-
reply: FastifyReply
|
|
170
|
-
) => Promise<void>
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const AuthPlugin: FastifyPluginAsync = async (server: FastifyInstance) => {
|
|
175
|
-
// Register JWT plugin
|
|
176
|
-
await server.register(jwt, {
|
|
177
|
-
secret: server.config.jwtSecret,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
// Add hasPermissions decorator
|
|
181
|
-
server.decorate('hasPermissions', (requiredPermissions: string[]) => {
|
|
182
|
-
return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
|
183
|
-
// JWT verification
|
|
184
|
-
try {
|
|
185
|
-
const payload = await request.jwtVerify()
|
|
186
|
-
request.token = payload as typeof request.token
|
|
187
|
-
} catch (error) {
|
|
188
|
-
throw new UnauthorizedError('Invalid token')
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Permission check
|
|
192
|
-
for (const permission of requiredPermissions) {
|
|
193
|
-
if (!request.token.permissions.includes(permission)) {
|
|
194
|
-
throw new ForbiddenError(`Missing permission: ${permission}`)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export default fp(AuthPlugin)
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## Server Composition
|
|
205
|
-
|
|
206
|
-
Assemble plugins in dependency order:
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import Fastify from 'fastify'
|
|
210
|
-
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
|
|
211
|
-
import ConfigPlugin from './plugins/config'
|
|
212
|
-
import AuthPlugin from './plugins/auth'
|
|
213
|
-
import RepoPlugin from './plugins/repo'
|
|
214
|
-
import ServicePlugin from './plugins/service'
|
|
215
|
-
import RouterPlugin from './plugins/router'
|
|
216
|
-
|
|
217
|
-
export const buildServer = async (opts?: {
|
|
218
|
-
repoPluginOpts?: RepoPluginOptions
|
|
219
|
-
servicePluginOpts?: ServicePluginOptions
|
|
220
|
-
}) => {
|
|
221
|
-
const server = Fastify({
|
|
222
|
-
logger: true,
|
|
223
|
-
requestIdHeader: 'x-request-id',
|
|
224
|
-
}).withTypeProvider<TypeBoxTypeProvider>()
|
|
225
|
-
|
|
226
|
-
// Register plugins in dependency order
|
|
227
|
-
await server.register(ConfigPlugin) // 1. Config (no dependencies)
|
|
228
|
-
await server.register(AuthPlugin) // 2. Auth (needs config)
|
|
229
|
-
await server.register(RepoPlugin, opts?.repoPluginOpts) // 3. Repos (needs config)
|
|
230
|
-
await server.register(ServicePlugin, opts?.servicePluginOpts) // 4. Services (needs repos)
|
|
231
|
-
await server.register(RouterPlugin, { prefix: '/api' }) // 5. Routes (needs services)
|
|
232
|
-
|
|
233
|
-
// Set global error handler
|
|
234
|
-
server.setErrorHandler(globalErrorHandler)
|
|
235
|
-
|
|
236
|
-
return server
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Start server
|
|
240
|
-
const server = await buildServer()
|
|
241
|
-
await server.listen({ port: 3000, host: '0.0.0.0' })
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## Testable Plugin Structure
|
|
245
|
-
|
|
246
|
-
Enable dependency injection for testing:
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
// test/server.test.ts
|
|
250
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
251
|
-
import { buildServer } from '../src/server'
|
|
252
|
-
import type { FastifyInstance } from 'fastify'
|
|
253
|
-
|
|
254
|
-
describe('User API', () => {
|
|
255
|
-
let server: FastifyInstance
|
|
256
|
-
|
|
257
|
-
beforeAll(async () => {
|
|
258
|
-
// Mock repos for testing
|
|
259
|
-
const mockUserRepo = {
|
|
260
|
-
getById: async (id: string) => mockUser,
|
|
261
|
-
create: async (entity: UserEntity) => entity,
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
server = await buildServer({
|
|
265
|
-
repoPluginOpts: {
|
|
266
|
-
repos: { userRepo: mockUserRepo as any },
|
|
267
|
-
},
|
|
268
|
-
})
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
afterAll(async () => {
|
|
272
|
-
await server.close()
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
it('should create a user', async () => {
|
|
276
|
-
const response = await server.inject({
|
|
277
|
-
method: 'POST',
|
|
278
|
-
url: '/api/users',
|
|
279
|
-
payload: { name: 'Alice', email: 'alice@example.com' },
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
expect(response.statusCode).toBe(201)
|
|
283
|
-
expect(response.json()).toMatchObject({
|
|
284
|
-
name: 'Alice',
|
|
285
|
-
email: 'alice@example.com',
|
|
286
|
-
})
|
|
287
|
-
})
|
|
288
|
-
})
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## Plugin with Lifecycle Hooks
|
|
292
|
-
|
|
293
|
-
Clean up resources on server shutdown:
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
import fp from 'fastify-plugin'
|
|
297
|
-
import type { FastifyInstance, FastifyPluginAsync } from 'fastify'
|
|
298
|
-
import { Pool } from 'pg'
|
|
299
|
-
import { drizzle } from 'drizzle-orm/node-postgres'
|
|
300
|
-
|
|
301
|
-
const DatabasePlugin: FastifyPluginAsync = async (server: FastifyInstance) => {
|
|
302
|
-
// Create connection pool
|
|
303
|
-
const pool = new Pool({ connectionString: server.config.databaseUrl })
|
|
304
|
-
const db = drizzle(pool)
|
|
305
|
-
|
|
306
|
-
server.decorate('db', db)
|
|
307
|
-
|
|
308
|
-
// Close pool on server shutdown
|
|
309
|
-
server.addHook('onClose', async (instance) => {
|
|
310
|
-
instance.log.info('Closing database connection pool')
|
|
311
|
-
await pool.end()
|
|
312
|
-
})
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export default fp(DatabasePlugin)
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
## Guidelines
|
|
319
|
-
|
|
320
|
-
1. **Use `fp()` wrapper** - Wrap utility plugins to prevent scope encapsulation
|
|
321
|
-
2. **Don't wrap routers** - Route plugins should remain encapsulated
|
|
322
|
-
3. **Register in dependency order** - Config → Auth → Repos → Services → Routes
|
|
323
|
-
4. **Extend FastifyInstance** - Use `declare module 'fastify'` for type safety
|
|
324
|
-
5. **Accept options** - Allow dependency injection via plugin options for testing
|
|
325
|
-
6. **Use decorators** - Add functionality to server with `server.decorate()`
|
|
326
|
-
7. **Cleanup in onClose** - Use lifecycle hooks to close connections, timers, etc.
|
|
327
|
-
8. **Async registration** - Use `await server.register()` for proper initialization order
|
|
328
|
-
9. **Type plugin options** - Define interface for plugin configuration
|
|
329
|
-
10. **Mock in tests** - Pass mock implementations via plugin options for unit testing
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
# TypeID - Type-Safe Prefixed Identifiers
|
|
2
|
-
|
|
3
|
-
TypeID provides type-safe, globally unique identifiers with human-readable prefixes (e.g., `usr_01h455vb4pex5vsknk084sn02q`).
|
|
4
|
-
|
|
5
|
-
## Core Concept
|
|
6
|
-
|
|
7
|
-
TypeID combines:
|
|
8
|
-
- **K-sortable** - IDs sort chronologically (based on timestamp)
|
|
9
|
-
- **Type-safe** - TypeScript prevents mixing user IDs with post IDs
|
|
10
|
-
- **Human-readable** - Prefix indicates the entity type
|
|
11
|
-
- **Globally unique** - Based on UUIDv7 standard
|
|
12
|
-
- **URL-safe** - Uses base32 encoding (no special characters)
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm i typeid-js
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Basic Usage
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
import { TypeID } from 'typeid-js'
|
|
24
|
-
|
|
25
|
-
// Define typed ID types
|
|
26
|
-
type UserID = TypeID<'usr'>
|
|
27
|
-
type OrgID = TypeID<'org'>
|
|
28
|
-
type PostID = TypeID<'pst'>
|
|
29
|
-
|
|
30
|
-
// Create new IDs
|
|
31
|
-
const userId = new TypeID('usr') // usr_01h455vb4pex5vsknk084sn02q
|
|
32
|
-
const orgId = new TypeID('org') // org_01h455w3x8k5z9y7q1m0n2b3c4
|
|
33
|
-
|
|
34
|
-
// Parse from string
|
|
35
|
-
const parsed = TypeID.fromString<'usr'>('usr_01h455vb4pex5vsknk084sn02q')
|
|
36
|
-
|
|
37
|
-
// Convert to string
|
|
38
|
-
const idString = userId.toString() // "usr_01h455vb4pex5vsknk084sn02q"
|
|
39
|
-
|
|
40
|
-
// Type safety prevents mixups
|
|
41
|
-
function getUser(id: UserID) { }
|
|
42
|
-
getUser(userId) // ✅ OK
|
|
43
|
-
getUser(orgId) // ❌ Type error - OrgID not assignable to UserID
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## TypeBox Schema with TypeID
|
|
47
|
-
|
|
48
|
-
Validate TypeID format in API schemas:
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
import { Type } from '@sinclair/typebox'
|
|
52
|
-
import { TypeID } from 'typeid-js'
|
|
53
|
-
|
|
54
|
-
// Schema with pattern validation
|
|
55
|
-
const UserIdParam = Type.Object({
|
|
56
|
-
userId: Type.String({
|
|
57
|
-
pattern: '^usr_[0-7][0-9a-hjkmnp-tv-z]{25}$',
|
|
58
|
-
description: 'User ID',
|
|
59
|
-
}),
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// Route with TypeID parameter
|
|
63
|
-
app.get('/users/:userId', {
|
|
64
|
-
schema: {
|
|
65
|
-
operationId: 'getUser',
|
|
66
|
-
tags: ['Users'],
|
|
67
|
-
params: UserIdParam,
|
|
68
|
-
response: {
|
|
69
|
-
200: UserSchema,
|
|
70
|
-
404: NotFoundErrorResponse,
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
}, async (request, reply) => {
|
|
74
|
-
// Parse string to TypeID
|
|
75
|
-
const userId = TypeID.fromString<'usr'>(request.params.userId)
|
|
76
|
-
const user = await userService.getById(userId)
|
|
77
|
-
return reply.send(user.toResponse())
|
|
78
|
-
})
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Entity Integration
|
|
82
|
-
|
|
83
|
-
Use TypeID in domain entities:
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
import { TypeID } from 'typeid-js'
|
|
87
|
-
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
|
|
88
|
-
import type { users } from './schema'
|
|
89
|
-
|
|
90
|
-
type UserID = TypeID<'usr'>
|
|
91
|
-
type UserRecord = InferSelectModel<typeof users>
|
|
92
|
-
type UserInsert = InferInsertModel<typeof users>
|
|
93
|
-
|
|
94
|
-
export class UserEntity {
|
|
95
|
-
public readonly id: UserID
|
|
96
|
-
public readonly name: string
|
|
97
|
-
public readonly email: string
|
|
98
|
-
|
|
99
|
-
private constructor(data: {
|
|
100
|
-
id: UserID
|
|
101
|
-
name: string
|
|
102
|
-
email: string
|
|
103
|
-
}) {
|
|
104
|
-
this.id = data.id
|
|
105
|
-
this.name = data.name
|
|
106
|
-
this.email = data.email
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
static fromRequest(rq: CreateUserRequest, id?: string): UserEntity {
|
|
110
|
-
return new UserEntity({
|
|
111
|
-
id: id ? TypeID.fromString<'usr'>(id) : new TypeID('usr'),
|
|
112
|
-
name: rq.name,
|
|
113
|
-
email: rq.email,
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
static fromRecord(record: UserRecord): UserEntity {
|
|
118
|
-
return new UserEntity({
|
|
119
|
-
id: TypeID.fromString<'usr'>(record.id),
|
|
120
|
-
name: record.name,
|
|
121
|
-
email: record.email,
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
toRecord(): UserInsert {
|
|
126
|
-
return {
|
|
127
|
-
id: this.id.toString(), // TypeID → string for database
|
|
128
|
-
name: this.name,
|
|
129
|
-
email: this.email,
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
toResponse(): UserResponse {
|
|
134
|
-
return {
|
|
135
|
-
id: this.id.toString(), // TypeID → string for API
|
|
136
|
-
name: this.name,
|
|
137
|
-
email: this.email,
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Drizzle Schema with TypeID
|
|
144
|
-
|
|
145
|
-
Store TypeIDs as text with type annotations:
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
import { pgTable, text, varchar, timestamp } from 'drizzle-orm/pg-core'
|
|
149
|
-
|
|
150
|
-
export const users = pgTable('users', {
|
|
151
|
-
// Store as TEXT, but type as string for inference
|
|
152
|
-
id: text('id').primaryKey().$type<string>(),
|
|
153
|
-
name: varchar('name', { length: 255 }).notNull(),
|
|
154
|
-
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
155
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
// Type inference works correctly
|
|
159
|
-
type UserRecord = InferSelectModel<typeof users>
|
|
160
|
-
// UserRecord.id is string, convert to TypeID in entity
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Multi-Tenancy with TypeID
|
|
164
|
-
|
|
165
|
-
Enforce organization-level isolation:
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
import { TypeID } from 'typeid-js'
|
|
169
|
-
|
|
170
|
-
type OrgID = TypeID<'org'>
|
|
171
|
-
type LedgerID = TypeID<'lgr'>
|
|
172
|
-
|
|
173
|
-
export class LedgerRepo {
|
|
174
|
-
constructor(private db: DrizzleDB) {}
|
|
175
|
-
|
|
176
|
-
async getById(orgId: OrgID, ledgerId: LedgerID): Promise<LedgerEntity> {
|
|
177
|
-
const record = await this.db.query.ledgers.findFirst({
|
|
178
|
-
where: and(
|
|
179
|
-
eq(ledgers.id, ledgerId.toString()),
|
|
180
|
-
eq(ledgers.organizationId, orgId.toString()) // Multi-tenancy check
|
|
181
|
-
),
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
if (!record) {
|
|
185
|
-
throw new NotFoundError('Ledger not found', {
|
|
186
|
-
organizationId: orgId.toString(),
|
|
187
|
-
ledgerId: ledgerId.toString(),
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return LedgerEntity.fromRecord(record)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
## Common Prefixes
|
|
197
|
-
|
|
198
|
-
Standardize prefixes across your application:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
// types/ids.ts
|
|
202
|
-
import type { TypeID } from 'typeid-js'
|
|
203
|
-
|
|
204
|
-
// User & Auth
|
|
205
|
-
export type UserID = TypeID<'usr'>
|
|
206
|
-
export type OrgID = TypeID<'org'>
|
|
207
|
-
export type TeamID = TypeID<'tem'>
|
|
208
|
-
export type ApiKeyID = TypeID<'key'>
|
|
209
|
-
|
|
210
|
-
// Content
|
|
211
|
-
export type PostID = TypeID<'pst'>
|
|
212
|
-
export type CommentID = TypeID<'cmt'>
|
|
213
|
-
export type TagID = TypeID<'tag'>
|
|
214
|
-
|
|
215
|
-
// Ledger (financial)
|
|
216
|
-
export type LedgerID = TypeID<'lgr'>
|
|
217
|
-
export type LedgerAccountID = TypeID<'lat'>
|
|
218
|
-
export type LedgerTransactionID = TypeID<'ltr'>
|
|
219
|
-
export type LedgerTransactionEntryID = TypeID<'lte'>
|
|
220
|
-
|
|
221
|
-
// Events
|
|
222
|
-
export type EventID = TypeID<'evt'>
|
|
223
|
-
export type JobID = TypeID<'job'>
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## TypeBox Schema Generator
|
|
227
|
-
|
|
228
|
-
Reusable helper for TypeID schemas:
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
import { Type, type TSchema } from '@sinclair/typebox'
|
|
232
|
-
|
|
233
|
-
// Generate TypeBox schema for any TypeID prefix
|
|
234
|
-
function TypeIDSchema<Prefix extends string>(prefix: Prefix) {
|
|
235
|
-
return Type.String({
|
|
236
|
-
pattern: `^${prefix}_[0-7][0-9a-hjkmnp-tv-z]{25}$`,
|
|
237
|
-
description: `${prefix.toUpperCase()} ID`,
|
|
238
|
-
})
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Usage
|
|
242
|
-
const UserIdSchema = TypeIDSchema('usr')
|
|
243
|
-
const OrgIdSchema = TypeIDSchema('org')
|
|
244
|
-
const PostIdSchema = TypeIDSchema('pst')
|
|
245
|
-
|
|
246
|
-
// In routes
|
|
247
|
-
const UserIdParam = Type.Object({
|
|
248
|
-
userId: UserIdSchema,
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
const LedgerIdParam = Type.Object({
|
|
252
|
-
orgId: OrgIdSchema,
|
|
253
|
-
ledgerId: TypeIDSchema('lgr'),
|
|
254
|
-
})
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## Error Context with TypeID
|
|
258
|
-
|
|
259
|
-
Include TypeIDs in error context for debugging:
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
import { TypeID } from 'typeid-js'
|
|
263
|
-
|
|
264
|
-
type ErrorContext = {
|
|
265
|
-
userId?: string
|
|
266
|
-
organizationId?: string
|
|
267
|
-
ledgerId?: string
|
|
268
|
-
[key: string]: unknown
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
class NotFoundError extends Error {
|
|
272
|
-
constructor(message: string, public readonly context?: ErrorContext) {
|
|
273
|
-
super(message)
|
|
274
|
-
this.name = 'NotFoundError'
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
toResponse() {
|
|
278
|
-
return {
|
|
279
|
-
type: 'NOT_FOUND',
|
|
280
|
-
status: 404,
|
|
281
|
-
title: 'Not Found',
|
|
282
|
-
detail: this.message,
|
|
283
|
-
...this.context, // Include IDs for debugging
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Usage
|
|
289
|
-
async getUser(userId: TypeID<'usr'>): Promise<UserEntity> {
|
|
290
|
-
const record = await this.db.query.users.findFirst({
|
|
291
|
-
where: eq(users.id, userId.toString()),
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
if (!record) {
|
|
295
|
-
throw new NotFoundError('User not found', {
|
|
296
|
-
userId: userId.toString(), // Include in error context
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return UserEntity.fromRecord(record)
|
|
301
|
-
}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
## Guidelines
|
|
305
|
-
|
|
306
|
-
1. **Use TypeID for all entity IDs** - Provides type safety and readability
|
|
307
|
-
2. **Define ID types** - Create typed aliases like `type UserID = TypeID<'usr'>`
|
|
308
|
-
3. **Store as text in DB** - Use `text('id').$type<string>()` in Drizzle schema
|
|
309
|
-
4. **Convert at boundaries** - Parse to TypeID in `fromRecord`, convert to string in `toRecord`
|
|
310
|
-
5. **Validate in schemas** - Use pattern matching to validate TypeID format in API
|
|
311
|
-
6. **Short prefixes** - Use 3-letter prefixes for readability (usr, org, pst)
|
|
312
|
-
7. **Consistent naming** - Standardize prefixes across your application
|
|
313
|
-
8. **Include in errors** - Add TypeIDs to error context for debugging
|
|
314
|
-
9. **Multi-tenancy** - Always include orgId in queries for tenant isolation
|
|
315
|
-
10. **Type safety** - Let TypeScript prevent ID mixups between different entity types
|