@martinffx/atelier 1.4.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +95 -220
  3. package/agents/architect.md +6 -6
  4. package/agents/oracle.md +1 -1
  5. package/agents/{recon.md → sentinel.md} +3 -3
  6. package/dist/atelier.js +167 -26
  7. package/package.json +2 -1
  8. package/skills/atelier-orchestrator/SKILL.md +180 -0
  9. package/skills/atelier-setup/SKILL.md +80 -0
  10. package/skills/code-review/SKILL.md +2 -2
  11. package/skills/code-review/references/reviewers.md +3 -3
  12. package/skills/code-review/references/rq.md +9 -9
  13. package/skills/code-subagents/SKILL.md +44 -75
  14. package/skills/code-subagents/references/batch-reviewer-prompt.md +44 -0
  15. package/skills/code-subagents/references/implementor-prompt.md +10 -11
  16. package/skills/spec-brainstorm/SKILL.md +21 -16
  17. package/skills/spec-finish/SKILL.md +14 -14
  18. package/skills/spec-implement/SKILL.md +42 -42
  19. package/skills/spec-plan/SKILL.md +72 -26
  20. package/skills/spec-plan/references/plan_template.md +77 -0
  21. package/skills/code-subagents/references/code-quality-reviewer-prompt.md +0 -48
  22. package/skills/code-subagents/references/spec-reviewer-prompt.md +0 -62
  23. package/skills/python-architecture/SKILL.md +0 -148
  24. package/skills/python-architecture/references/data-modeling.md +0 -558
  25. package/skills/python-architecture/references/ddd.md +0 -569
  26. package/skills/python-architecture/references/functional-core.md +0 -337
  27. package/skills/python-build-tools/SKILL.md +0 -153
  28. package/skills/python-build-tools/references/basedpyright.md +0 -321
  29. package/skills/python-build-tools/references/ruff.md +0 -225
  30. package/skills/python-build-tools/references/uv.md +0 -257
  31. package/skills/python-fastapi/SKILL.md +0 -184
  32. package/skills/python-fastapi/references/api-design.md +0 -563
  33. package/skills/python-fastapi/references/dependencies.md +0 -479
  34. package/skills/python-fastapi/references/middleware.md +0 -475
  35. package/skills/python-fastapi/references/validation.md +0 -497
  36. package/skills/python-modern-python/SKILL.md +0 -201
  37. package/skills/python-modern-python/references/async.md +0 -76
  38. package/skills/python-modern-python/references/pattern-matching.md +0 -89
  39. package/skills/python-modern-python/references/typing.md +0 -96
  40. package/skills/python-monorepo/SKILL.md +0 -146
  41. package/skills/python-monorepo/references/docker.md +0 -515
  42. package/skills/python-monorepo/references/namespace-packages.md +0 -426
  43. package/skills/python-monorepo/references/workspace-config.md +0 -341
  44. package/skills/python-sqlalchemy/SKILL.md +0 -119
  45. package/skills/python-sqlalchemy/references/async.md +0 -51
  46. package/skills/python-sqlalchemy/references/models.md +0 -67
  47. package/skills/python-sqlalchemy/references/queries.md +0 -57
  48. package/skills/python-temporal/SKILL.md +0 -128
  49. package/skills/python-temporal/references/patterns.md +0 -87
  50. package/skills/python-temporal/references/testing.md +0 -54
  51. package/skills/python-testing/SKILL.md +0 -161
  52. package/skills/python-testing/references/boundaries.md +0 -449
  53. package/skills/python-testing/references/mocking.md +0 -449
  54. package/skills/python-testing/references/pytest.md +0 -524
  55. package/skills/spec-orchestrator/SKILL.md +0 -145
  56. package/skills/typescript-api-design/SKILL.md +0 -396
  57. package/skills/typescript-api-design/references/error-responses.md +0 -469
  58. package/skills/typescript-build-tools/SKILL.md +0 -447
  59. package/skills/typescript-build-tools/references/biome.md +0 -436
  60. package/skills/typescript-build-tools/references/bun.md +0 -432
  61. package/skills/typescript-build-tools/references/turborepo.md +0 -426
  62. package/skills/typescript-build-tools/references/vitest.md +0 -518
  63. package/skills/typescript-drizzle-orm/SKILL.md +0 -398
  64. package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
  65. package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
  66. package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
  67. package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
  68. package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
  69. package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
  70. package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
  71. package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
  72. package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
  73. package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
  74. package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
  75. package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
  76. package/skills/typescript-effect-ts/SKILL.md +0 -341
  77. package/skills/typescript-effect-ts/references/core.md +0 -412
  78. package/skills/typescript-effect-ts/references/error-handling.md +0 -468
  79. package/skills/typescript-effect-ts/references/resources.md +0 -476
  80. package/skills/typescript-effect-ts/references/schema.md +0 -536
  81. package/skills/typescript-effect-ts/references/services.md +0 -461
  82. package/skills/typescript-fastify/SKILL.md +0 -405
  83. package/skills/typescript-fastify/references/plugins.md +0 -329
  84. package/skills/typescript-fastify/references/typeid.md +0 -315
  85. package/skills/typescript-functional-patterns/SKILL.md +0 -402
  86. package/skills/typescript-functional-patterns/references/adts.md +0 -497
  87. package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
  88. package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
  89. package/skills/typescript-functional-patterns/references/option-result.md +0 -585
  90. package/skills/typescript-testing/SKILL.md +0 -353
  91. package/skills/typescript-testing/references/mocking.md +0 -675
  92. package/skills/typescript-testing/references/msw.md +0 -852
  93. 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