@stravigor/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +45 -0
  2. package/package.json +83 -0
  3. package/src/auth/access_token.ts +122 -0
  4. package/src/auth/auth.ts +86 -0
  5. package/src/auth/index.ts +7 -0
  6. package/src/auth/middleware/authenticate.ts +64 -0
  7. package/src/auth/middleware/csrf.ts +62 -0
  8. package/src/auth/middleware/guest.ts +46 -0
  9. package/src/broadcast/broadcast_manager.ts +411 -0
  10. package/src/broadcast/client.ts +302 -0
  11. package/src/broadcast/index.ts +58 -0
  12. package/src/cache/cache_manager.ts +56 -0
  13. package/src/cache/cache_store.ts +31 -0
  14. package/src/cache/helpers.ts +74 -0
  15. package/src/cache/http_cache.ts +109 -0
  16. package/src/cache/index.ts +6 -0
  17. package/src/cache/memory_store.ts +63 -0
  18. package/src/cli/bootstrap.ts +37 -0
  19. package/src/cli/commands/generate_api.ts +74 -0
  20. package/src/cli/commands/generate_key.ts +46 -0
  21. package/src/cli/commands/generate_models.ts +48 -0
  22. package/src/cli/commands/migration_compare.ts +152 -0
  23. package/src/cli/commands/migration_fresh.ts +123 -0
  24. package/src/cli/commands/migration_generate.ts +79 -0
  25. package/src/cli/commands/migration_rollback.ts +53 -0
  26. package/src/cli/commands/migration_run.ts +44 -0
  27. package/src/cli/commands/queue_flush.ts +35 -0
  28. package/src/cli/commands/queue_retry.ts +34 -0
  29. package/src/cli/commands/queue_work.ts +40 -0
  30. package/src/cli/commands/scheduler_work.ts +45 -0
  31. package/src/cli/strav.ts +33 -0
  32. package/src/config/configuration.ts +105 -0
  33. package/src/config/loaders/base_loader.ts +69 -0
  34. package/src/config/loaders/env_loader.ts +112 -0
  35. package/src/config/loaders/typescript_loader.ts +56 -0
  36. package/src/config/types.ts +8 -0
  37. package/src/core/application.ts +4 -0
  38. package/src/core/container.ts +117 -0
  39. package/src/core/index.ts +3 -0
  40. package/src/core/inject.ts +39 -0
  41. package/src/database/database.ts +54 -0
  42. package/src/database/index.ts +30 -0
  43. package/src/database/introspector.ts +446 -0
  44. package/src/database/migration/differ.ts +308 -0
  45. package/src/database/migration/file_generator.ts +125 -0
  46. package/src/database/migration/index.ts +18 -0
  47. package/src/database/migration/runner.ts +133 -0
  48. package/src/database/migration/sql_generator.ts +378 -0
  49. package/src/database/migration/tracker.ts +76 -0
  50. package/src/database/migration/types.ts +189 -0
  51. package/src/database/query_builder.ts +474 -0
  52. package/src/encryption/encryption_manager.ts +209 -0
  53. package/src/encryption/helpers.ts +158 -0
  54. package/src/encryption/index.ts +3 -0
  55. package/src/encryption/types.ts +6 -0
  56. package/src/events/emitter.ts +101 -0
  57. package/src/events/index.ts +2 -0
  58. package/src/exceptions/errors.ts +75 -0
  59. package/src/exceptions/exception_handler.ts +126 -0
  60. package/src/exceptions/helpers.ts +25 -0
  61. package/src/exceptions/http_exception.ts +129 -0
  62. package/src/exceptions/index.ts +23 -0
  63. package/src/exceptions/strav_error.ts +11 -0
  64. package/src/generators/api_generator.ts +972 -0
  65. package/src/generators/config.ts +87 -0
  66. package/src/generators/doc_generator.ts +974 -0
  67. package/src/generators/index.ts +11 -0
  68. package/src/generators/model_generator.ts +586 -0
  69. package/src/generators/route_generator.ts +188 -0
  70. package/src/generators/test_generator.ts +1666 -0
  71. package/src/helpers/crypto.ts +4 -0
  72. package/src/helpers/env.ts +50 -0
  73. package/src/helpers/identity.ts +12 -0
  74. package/src/helpers/index.ts +4 -0
  75. package/src/helpers/strings.ts +67 -0
  76. package/src/http/context.ts +215 -0
  77. package/src/http/cookie.ts +59 -0
  78. package/src/http/cors.ts +163 -0
  79. package/src/http/index.ts +16 -0
  80. package/src/http/middleware.ts +39 -0
  81. package/src/http/rate_limit.ts +173 -0
  82. package/src/http/router.ts +556 -0
  83. package/src/http/server.ts +79 -0
  84. package/src/i18n/defaults/en/validation.json +20 -0
  85. package/src/i18n/helpers.ts +72 -0
  86. package/src/i18n/i18n_manager.ts +155 -0
  87. package/src/i18n/index.ts +4 -0
  88. package/src/i18n/middleware.ts +90 -0
  89. package/src/i18n/translator.ts +96 -0
  90. package/src/i18n/types.ts +17 -0
  91. package/src/logger/index.ts +6 -0
  92. package/src/logger/logger.ts +100 -0
  93. package/src/logger/request_logger.ts +19 -0
  94. package/src/logger/sinks/console_sink.ts +24 -0
  95. package/src/logger/sinks/file_sink.ts +24 -0
  96. package/src/logger/sinks/sink.ts +36 -0
  97. package/src/mail/css_inliner.ts +79 -0
  98. package/src/mail/helpers.ts +212 -0
  99. package/src/mail/index.ts +19 -0
  100. package/src/mail/mail_manager.ts +92 -0
  101. package/src/mail/transports/log_transport.ts +69 -0
  102. package/src/mail/transports/resend_transport.ts +59 -0
  103. package/src/mail/transports/sendgrid_transport.ts +77 -0
  104. package/src/mail/transports/smtp_transport.ts +48 -0
  105. package/src/mail/types.ts +80 -0
  106. package/src/notification/base_notification.ts +67 -0
  107. package/src/notification/channels/database_channel.ts +30 -0
  108. package/src/notification/channels/discord_channel.ts +43 -0
  109. package/src/notification/channels/email_channel.ts +37 -0
  110. package/src/notification/channels/webhook_channel.ts +45 -0
  111. package/src/notification/helpers.ts +214 -0
  112. package/src/notification/index.ts +20 -0
  113. package/src/notification/notification_manager.ts +126 -0
  114. package/src/notification/types.ts +122 -0
  115. package/src/orm/base_model.ts +351 -0
  116. package/src/orm/decorators.ts +127 -0
  117. package/src/orm/index.ts +4 -0
  118. package/src/policy/authorize.ts +44 -0
  119. package/src/policy/index.ts +3 -0
  120. package/src/policy/policy_result.ts +13 -0
  121. package/src/queue/index.ts +11 -0
  122. package/src/queue/queue.ts +338 -0
  123. package/src/queue/worker.ts +197 -0
  124. package/src/scheduler/cron.ts +140 -0
  125. package/src/scheduler/index.ts +7 -0
  126. package/src/scheduler/runner.ts +116 -0
  127. package/src/scheduler/schedule.ts +183 -0
  128. package/src/scheduler/scheduler.ts +47 -0
  129. package/src/schema/database_representation.ts +122 -0
  130. package/src/schema/define_association.ts +60 -0
  131. package/src/schema/define_schema.ts +46 -0
  132. package/src/schema/field_builder.ts +155 -0
  133. package/src/schema/field_definition.ts +66 -0
  134. package/src/schema/index.ts +21 -0
  135. package/src/schema/naming.ts +19 -0
  136. package/src/schema/postgres.ts +109 -0
  137. package/src/schema/registry.ts +157 -0
  138. package/src/schema/representation_builder.ts +479 -0
  139. package/src/schema/type_builder.ts +107 -0
  140. package/src/schema/types.ts +35 -0
  141. package/src/session/index.ts +4 -0
  142. package/src/session/middleware.ts +46 -0
  143. package/src/session/session.ts +308 -0
  144. package/src/session/session_manager.ts +81 -0
  145. package/src/storage/index.ts +13 -0
  146. package/src/storage/local_driver.ts +46 -0
  147. package/src/storage/s3_driver.ts +51 -0
  148. package/src/storage/storage.ts +43 -0
  149. package/src/storage/storage_manager.ts +59 -0
  150. package/src/storage/types.ts +42 -0
  151. package/src/storage/upload.ts +91 -0
  152. package/src/validation/index.ts +18 -0
  153. package/src/validation/rules.ts +170 -0
  154. package/src/validation/validate.ts +41 -0
  155. package/src/view/cache.ts +47 -0
  156. package/src/view/client/islands.ts +50 -0
  157. package/src/view/compiler.ts +185 -0
  158. package/src/view/engine.ts +139 -0
  159. package/src/view/escape.ts +14 -0
  160. package/src/view/index.ts +13 -0
  161. package/src/view/islands/island_builder.ts +161 -0
  162. package/src/view/islands/vue_plugin.ts +140 -0
  163. package/src/view/middleware/static.ts +35 -0
  164. package/src/view/tokenizer.ts +172 -0
  165. package/tsconfig.json +4 -0
@@ -0,0 +1,974 @@
1
+ import { join } from 'node:path'
2
+ import { Archetype } from '../schema/types.ts'
3
+ import type { SchemaDefinition } from '../schema/types.ts'
4
+ import type {
5
+ DatabaseRepresentation,
6
+ TableDefinition,
7
+ ColumnDefinition,
8
+ } from '../schema/database_representation.ts'
9
+ import type { FieldDefinition, FieldValidator } from '../schema/field_definition.ts'
10
+ import type { PostgreSQLCustomType } from '../schema/postgres.ts'
11
+ import { toSnakeCase, toCamelCase, toPascalCase, pluralize } from '../helpers/strings.ts'
12
+ import type { GeneratedFile } from './model_generator.ts'
13
+ import type { GeneratorConfig, GeneratorPaths } from './config.ts'
14
+ import { resolvePaths } from './config.ts'
15
+ import { ApiRouting, toRouteSegment, toChildSegment } from './route_generator.ts'
16
+ import type { ApiRoutingConfig } from './route_generator.ts'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Archetype behaviour (mirrored from api_generator.ts)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const ARCHETYPE_CONTROLLER: Record<Archetype, string[]> = {
23
+ [Archetype.Entity]: ['index', 'show', 'store', 'update', 'destroy'],
24
+ [Archetype.Attribute]: ['index', 'show', 'store', 'update', 'destroy'],
25
+ [Archetype.Contribution]: ['index', 'show', 'store', 'update', 'destroy'],
26
+ [Archetype.Reference]: ['index', 'show', 'store', 'update', 'destroy'],
27
+ [Archetype.Component]: ['index', 'show', 'update'],
28
+ [Archetype.Event]: ['index', 'show', 'store'],
29
+ [Archetype.Configuration]: ['show', 'update', 'destroy'],
30
+ [Archetype.Association]: [],
31
+ }
32
+
33
+ const DEPENDENT_ARCHETYPES: Set<Archetype> = new Set([
34
+ Archetype.Component,
35
+ Archetype.Attribute,
36
+ Archetype.Event,
37
+ Archetype.Configuration,
38
+ Archetype.Contribution,
39
+ ])
40
+
41
+ const SYSTEM_COLUMNS = new Set(['id', 'created_at', 'updated_at', 'deleted_at'])
42
+
43
+ const ARCHETYPE_DESCRIPTIONS: Record<Archetype, string> = {
44
+ [Archetype.Entity]:
45
+ 'A standalone entity with full CRUD operations and soft delete support.',
46
+ [Archetype.Contribution]:
47
+ 'A user-contributed resource under a parent entity. Full CRUD with soft delete.',
48
+ [Archetype.Reference]:
49
+ 'A lookup/reference table with full CRUD. No soft delete &mdash; records are permanently removed.',
50
+ [Archetype.Attribute]:
51
+ 'A dependent attribute of a parent entity. Full CRUD with soft delete.',
52
+ [Archetype.Component]:
53
+ 'A tightly-coupled component of a parent entity. Can be listed, viewed, and updated &mdash; but not independently created or deleted.',
54
+ [Archetype.Event]:
55
+ 'An append-only event log under a parent entity. Events can be listed, viewed, and appended &mdash; but never updated or deleted.',
56
+ [Archetype.Configuration]:
57
+ 'A singleton configuration record under a parent entity. One record per parent. Supports show, upsert, and reset.',
58
+ [Archetype.Association]: 'A join table linking two entities.',
59
+ }
60
+
61
+ const API_DEFAULTS: ApiRoutingConfig = {
62
+ routing: ApiRouting.Prefix,
63
+ prefix: '/api',
64
+ subdomain: 'api',
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // DocGenerator
69
+ // ---------------------------------------------------------------------------
70
+
71
+ export default class DocGenerator {
72
+ private apiConfig: ApiRoutingConfig
73
+ private paths: GeneratorPaths
74
+ private schemaMap: Map<string, SchemaDefinition>
75
+
76
+ constructor(
77
+ private schemas: SchemaDefinition[],
78
+ private representation: DatabaseRepresentation,
79
+ config?: GeneratorConfig,
80
+ apiConfig?: Partial<ApiRoutingConfig>
81
+ ) {
82
+ this.apiConfig = { ...API_DEFAULTS, ...apiConfig }
83
+ this.paths = resolvePaths(config)
84
+ this.schemaMap = new Map(schemas.map(s => [s.name, s]))
85
+ }
86
+
87
+ generate(): GeneratedFile[] {
88
+ return [this.generateIndexPage()]
89
+ }
90
+
91
+ async writeAll(): Promise<GeneratedFile[]> {
92
+ const files = this.generate()
93
+ for (const file of files) {
94
+ await Bun.write(file.path, file.content)
95
+ }
96
+ return files
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Main page
101
+ // ---------------------------------------------------------------------------
102
+
103
+ private generateIndexPage(): GeneratedFile {
104
+ const routable = this.schemas.filter(s => s.archetype !== Archetype.Association)
105
+
106
+ // Group by parent for sidebar
107
+ const rootSchemas: SchemaDefinition[] = []
108
+ const childrenOf = new Map<string, SchemaDefinition[]>()
109
+
110
+ for (const s of routable) {
111
+ if (DEPENDENT_ARCHETYPES.has(s.archetype) && s.parent) {
112
+ if (!childrenOf.has(s.parent)) childrenOf.set(s.parent, [])
113
+ childrenOf.get(s.parent)!.push(s)
114
+ } else {
115
+ rootSchemas.push(s)
116
+ }
117
+ }
118
+
119
+ const sidebar = this.buildSidebar(rootSchemas, childrenOf)
120
+ const content = this.buildContent(routable)
121
+
122
+ const html = `<!DOCTYPE html>
123
+ <html lang="en" class="scroll-smooth">
124
+ <head>
125
+ <meta charset="UTF-8">
126
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
127
+ <title>API Reference</title>
128
+ <script src="https://cdn.tailwindcss.com"></script>
129
+ <link rel="preconnect" href="https://fonts.googleapis.com">
130
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
131
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
132
+ <script>
133
+ tailwind.config = {
134
+ theme: {
135
+ extend: {
136
+ fontFamily: {
137
+ sans: ['Inter', 'system-ui', 'sans-serif'],
138
+ mono: ['JetBrains Mono', 'monospace'],
139
+ },
140
+ },
141
+ },
142
+ }
143
+ </script>
144
+ <style>
145
+ html { scroll-padding-top: 6rem; }
146
+ .sidebar-link.active { color: #fff; background: rgba(255,255,255,0.06); }
147
+ .sidebar-link:hover { color: #e4e4e7; background: rgba(255,255,255,0.04); }
148
+ .method-badge { font-size: 0.65rem; font-weight: 600; letter-spacing: 0.05em; padding: 0.15rem 0.5rem; border-radius: 9999px; text-transform: uppercase; }
149
+ pre code { font-size: 0.8125rem; line-height: 1.625; }
150
+ @media (max-width: 1023px) {
151
+ .sidebar { transform: translateX(-100%); position: fixed; z-index: 50; }
152
+ .sidebar.open { transform: translateX(0); }
153
+ }
154
+ </style>
155
+ </head>
156
+ <body class="bg-white font-sans text-zinc-900 antialiased">
157
+
158
+ <!-- Mobile menu button -->
159
+ <button onclick="document.getElementById('sidebar').classList.toggle('open')"
160
+ class="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-lg bg-zinc-900 text-white shadow-lg">
161
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
162
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
163
+ </svg>
164
+ </button>
165
+
166
+ <!-- Sidebar -->
167
+ <aside id="sidebar" class="sidebar fixed inset-y-0 left-0 w-72 bg-zinc-900 overflow-y-auto transition-transform duration-200 lg:translate-x-0">
168
+ <div class="px-6 py-8">
169
+ <h1 class="text-lg font-semibold text-white tracking-tight">API Reference</h1>
170
+ <p class="mt-1 text-xs text-zinc-500">Generated by Strav</p>
171
+ </div>
172
+ <nav class="px-3 pb-8">
173
+ ${sidebar}
174
+ </nav>
175
+ </aside>
176
+
177
+ <!-- Main content -->
178
+ <main class="lg:pl-72">
179
+ <div class="max-w-4xl mx-auto px-6 sm:px-8 py-16">
180
+ ${content}
181
+ </div>
182
+ </main>
183
+
184
+ <script>
185
+ // Highlight active sidebar link on scroll
186
+ const observer = new IntersectionObserver(entries => {
187
+ for (const entry of entries) {
188
+ if (entry.isIntersecting) {
189
+ document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
190
+ const link = document.querySelector('.sidebar-link[href="#' + entry.target.id + '"]');
191
+ if (link) link.classList.add('active');
192
+ }
193
+ }
194
+ }, { rootMargin: '-20% 0px -70% 0px' });
195
+ document.querySelectorAll('section[id]').forEach(s => observer.observe(s));
196
+
197
+ // Close mobile sidebar on link click
198
+ document.querySelectorAll('.sidebar-link').forEach(link => {
199
+ link.addEventListener('click', () => {
200
+ document.getElementById('sidebar').classList.remove('open');
201
+ });
202
+ });
203
+ </script>
204
+ </body>
205
+ </html>`
206
+
207
+ return {
208
+ path: join(this.paths.docs, 'index.html'),
209
+ content: html,
210
+ }
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Sidebar
215
+ // ---------------------------------------------------------------------------
216
+
217
+ private buildSidebar(
218
+ rootSchemas: SchemaDefinition[],
219
+ childrenOf: Map<string, SchemaDefinition[]>
220
+ ): string {
221
+ const lines: string[] = []
222
+
223
+ // Introduction & Auth
224
+ lines.push(' <div class="mb-6">')
225
+ lines.push(' <p class="px-3 text-[0.6875rem] font-semibold uppercase tracking-wider text-zinc-500 mb-2">Getting Started</p>')
226
+ lines.push(' <a href="#introduction" class="sidebar-link block px-3 py-1.5 text-sm text-zinc-400 rounded-md transition-colors">Introduction</a>')
227
+ lines.push(' <a href="#authentication" class="sidebar-link block px-3 py-1.5 text-sm text-zinc-400 rounded-md transition-colors">Authentication</a>')
228
+ lines.push(' </div>')
229
+
230
+ // Resources
231
+ lines.push(' <div>')
232
+ lines.push(' <p class="px-3 text-[0.6875rem] font-semibold uppercase tracking-wider text-zinc-500 mb-2">Resources</p>')
233
+
234
+ const emitSchema = (schema: SchemaDefinition): void => {
235
+ const anchor = toSnakeCase(schema.name)
236
+ const label = this.displayName(schema.name)
237
+ lines.push(` <a href="#${anchor}" class="sidebar-link block px-3 py-1.5 text-sm text-zinc-400 rounded-md transition-colors">${label}</a>`)
238
+
239
+ const children = childrenOf.get(schema.name)
240
+ if (children?.length) {
241
+ for (const child of children) emitSchema(child)
242
+ }
243
+ }
244
+
245
+ for (const schema of rootSchemas) emitSchema(schema)
246
+
247
+ lines.push(' </div>')
248
+ return lines.join('\n')
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Main content
253
+ // ---------------------------------------------------------------------------
254
+
255
+ private buildContent(routable: SchemaDefinition[]): string {
256
+ const sections: string[] = []
257
+
258
+ sections.push(this.buildIntroduction())
259
+ sections.push(this.buildAuthentication())
260
+
261
+ for (const schema of routable) {
262
+ const table = this.representation.tables.find(t => t.name === toSnakeCase(schema.name))
263
+ if (!table) continue
264
+ sections.push(this.buildResourceSection(schema, table))
265
+ }
266
+
267
+ return sections.join('\n\n')
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Introduction section
272
+ // ---------------------------------------------------------------------------
273
+
274
+ private buildIntroduction(): string {
275
+ const baseUrl =
276
+ this.apiConfig.routing === ApiRouting.Subdomain
277
+ ? `https://${this.apiConfig.subdomain}.&lt;domain&gt;`
278
+ : `https://&lt;domain&gt;${this.apiConfig.prefix}`
279
+
280
+ return ` <section id="introduction" class="mb-20">
281
+ <h2 class="text-2xl font-semibold tracking-tight text-zinc-900">Introduction</h2>
282
+ <div class="mt-4 text-sm leading-relaxed text-zinc-600 space-y-3">
283
+ <p>Welcome to the API reference. This documentation is auto-generated from the application schema definitions.</p>
284
+ <div class="rounded-lg border border-zinc-200 overflow-hidden">
285
+ <table class="w-full text-sm">
286
+ <tbody>
287
+ <tr class="border-b border-zinc-100">
288
+ <td class="px-4 py-2.5 font-medium text-zinc-700 bg-zinc-50 w-40">Base URL</td>
289
+ <td class="px-4 py-2.5 font-mono text-xs text-zinc-600">${baseUrl}</td>
290
+ </tr>
291
+ <tr class="border-b border-zinc-100">
292
+ <td class="px-4 py-2.5 font-medium text-zinc-700 bg-zinc-50">Content-Type</td>
293
+ <td class="px-4 py-2.5 font-mono text-xs text-zinc-600">application/json</td>
294
+ </tr>
295
+ <tr>
296
+ <td class="px-4 py-2.5 font-medium text-zinc-700 bg-zinc-50">Authentication</td>
297
+ <td class="px-4 py-2.5 font-mono text-xs text-zinc-600">Bearer token</td>
298
+ </tr>
299
+ </tbody>
300
+ </table>
301
+ </div>
302
+ </div>
303
+ </section>`
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Authentication section
308
+ // ---------------------------------------------------------------------------
309
+
310
+ private buildAuthentication(): string {
311
+ return ` <section id="authentication" class="mb-20">
312
+ <h2 class="text-2xl font-semibold tracking-tight text-zinc-900">Authentication</h2>
313
+ <div class="mt-4 text-sm leading-relaxed text-zinc-600 space-y-4">
314
+ <p>All API endpoints require authentication via a bearer token. Include the token in the <code class="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">Authorization</code> header of every request.</p>
315
+ <div class="rounded-lg bg-zinc-900 p-4 overflow-x-auto">
316
+ <pre><code class="text-zinc-100 font-mono">GET ${this.apiConfig.routing === ApiRouting.Prefix ? this.apiConfig.prefix : ''}/resources HTTP/1.1
317
+ Host: &lt;domain&gt;
318
+ Authorization: Bearer &lt;your-token&gt;
319
+ Content-Type: application/json</code></pre>
320
+ </div>
321
+ <div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3">
322
+ <p class="text-amber-800 text-xs font-medium">Unauthenticated requests</p>
323
+ <p class="text-amber-700 text-xs mt-1">Requests without a valid bearer token will receive a <code class="font-mono bg-amber-100 px-1 py-0.5 rounded">401 Unauthenticated</code> response.</p>
324
+ </div>
325
+ <h3 class="text-base font-semibold text-zinc-900 pt-2">Error responses</h3>
326
+ <div class="rounded-lg bg-zinc-900 p-4 overflow-x-auto">
327
+ <pre><code class="text-zinc-100 font-mono">// 401 Unauthenticated
328
+ { "error": "Unauthenticated" }
329
+
330
+ // 422 Validation Error
331
+ { "errors": { "fieldName": ["Validation message"] } }
332
+
333
+ // 404 Not Found
334
+ { "error": "Not Found" }</code></pre>
335
+ </div>
336
+ </div>
337
+ </section>`
338
+ }
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Per-resource section
342
+ // ---------------------------------------------------------------------------
343
+
344
+ private buildResourceSection(
345
+ schema: SchemaDefinition,
346
+ table: TableDefinition
347
+ ): string {
348
+ const anchor = toSnakeCase(schema.name)
349
+ const label = this.displayName(schema.name)
350
+ const actions = ARCHETYPE_CONTROLLER[schema.archetype] ?? []
351
+ const archetypeColor = this.archetypeBadgeColor(schema.archetype)
352
+ const description = ARCHETYPE_DESCRIPTIONS[schema.archetype]
353
+
354
+ const fieldsTable = this.buildFieldsTable(schema, table)
355
+ const endpoints = actions
356
+ .map(action => this.buildEndpoint(action, schema, table))
357
+ .join('\n')
358
+
359
+ return ` <section id="${anchor}" class="mb-20">
360
+ <div class="flex items-center gap-3">
361
+ <h2 class="text-2xl font-semibold tracking-tight text-zinc-900">${label}</h2>
362
+ <span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-[0.625rem] font-semibold ${archetypeColor}">${schema.archetype}</span>
363
+ </div>
364
+ <p class="mt-2 text-sm text-zinc-500">${description}</p>
365
+ ${schema.parent ? ` <p class="mt-4 text-sm text-zinc-500">Parent: <span class="font-semibold text-zinc-700">${this.displayName(schema.parent)}</span></p>` : ''}
366
+
367
+ <div class="mt-6">
368
+ <h3 class="text-sm font-semibold text-zinc-900 mb-3">Fields</h3>
369
+ ${fieldsTable}
370
+ </div>
371
+
372
+ <div class="mt-8 space-y-8">
373
+ <h3 class="text-sm font-semibold text-zinc-900">Endpoints</h3>
374
+ ${endpoints}
375
+ </div>
376
+ </section>`
377
+ }
378
+
379
+ // ---------------------------------------------------------------------------
380
+ // Fields table
381
+ // ---------------------------------------------------------------------------
382
+
383
+ private buildFieldsTable(schema: SchemaDefinition, table: TableDefinition): string {
384
+ const rows: string[] = []
385
+
386
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
387
+ if (fieldDef.primaryKey) continue
388
+ const colName = toSnakeCase(fieldName)
389
+ if (SYSTEM_COLUMNS.has(colName)) continue
390
+
391
+ const camelName = toCamelCase(fieldName)
392
+ const typeLabel = this.fieldTypeLabel(fieldDef)
393
+ const required = fieldDef.required
394
+ ? '<span class="text-rose-500 text-[0.625rem] font-semibold">required</span>'
395
+ : '<span class="text-zinc-400 text-[0.625rem]">optional</span>'
396
+ const sensitive = fieldDef.sensitive
397
+ ? ' <span class="text-amber-500 text-[0.625rem] font-semibold">sensitive</span>'
398
+ : ''
399
+ const validators = this.validatorSummary(fieldDef)
400
+ const validatorHtml = validators
401
+ ? `<span class="text-zinc-400 text-[0.625rem]">${validators}</span>`
402
+ : ''
403
+
404
+ rows.push(` <tr class="border-b border-zinc-100 last:border-0">
405
+ <td class="py-2.5 pl-4 pr-4 font-mono text-xs text-emerald-600 whitespace-nowrap">${camelName}</td>
406
+ <td class="py-2.5 pr-4 text-xs text-zinc-500 whitespace-nowrap">${typeLabel}</td>
407
+ <td class="py-2.5 pr-4">${required}${sensitive}</td>
408
+ <td class="py-2.5 text-xs text-zinc-400">${validatorHtml}</td>
409
+ </tr>`)
410
+ }
411
+
412
+ if (rows.length === 0) {
413
+ return ' <p class="text-xs text-zinc-400 italic">No user-editable fields.</p>'
414
+ }
415
+
416
+ return ` <div class="rounded-lg border border-zinc-200 overflow-hidden">
417
+ <table class="w-full text-sm">
418
+ <thead>
419
+ <tr class="bg-zinc-50 border-b border-zinc-200">
420
+ <th class="text-left px-4 py-2 text-[0.6875rem] font-semibold text-zinc-500 uppercase tracking-wider">Name</th>
421
+ <th class="text-left px-4 py-2 text-[0.6875rem] font-semibold text-zinc-500 uppercase tracking-wider">Type</th>
422
+ <th class="text-left px-4 py-2 text-[0.6875rem] font-semibold text-zinc-500 uppercase tracking-wider">Status</th>
423
+ <th class="text-left px-4 py-2 text-[0.6875rem] font-semibold text-zinc-500 uppercase tracking-wider">Constraints</th>
424
+ </tr>
425
+ </thead>
426
+ <tbody class="px-4">
427
+ ${rows.join('\n')}
428
+ </tbody>
429
+ </table>
430
+ </div>`
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // Per-endpoint block
435
+ // ---------------------------------------------------------------------------
436
+
437
+ private buildEndpoint(
438
+ action: string,
439
+ schema: SchemaDefinition,
440
+ table: TableDefinition
441
+ ): string {
442
+ const { method, pathPattern, description } = this.actionMeta(action, schema)
443
+ const methodColor = this.methodBadgeColor(method)
444
+ const anchor = `${toSnakeCase(schema.name)}-${action}`
445
+
446
+ const bodyFields = this.endpointBodyFields(action, schema, table)
447
+ const bodySection = bodyFields.length > 0 ? this.buildBodyFieldsTable(bodyFields) : ''
448
+
449
+ const exampleRequest = this.buildExampleRequest(method, pathPattern, action, schema, table)
450
+ const exampleResponse = this.buildExampleResponse(action, schema, table)
451
+
452
+ return ` <div id="${anchor}" class="rounded-lg border border-zinc-200 overflow-hidden">
453
+ <div class="flex items-center gap-3 px-4 py-3 bg-zinc-50 border-b border-zinc-200">
454
+ <span class="method-badge ${methodColor}">${method}</span>
455
+ <code class="text-sm font-mono text-zinc-700">${this.escapeHtml(pathPattern)}</code>
456
+ </div>
457
+ <div class="px-4 py-4 space-y-4">
458
+ <p class="text-sm text-zinc-600">${description}</p>
459
+ ${bodySection}
460
+ ${exampleRequest}
461
+ ${exampleResponse}
462
+ </div>
463
+ </div>`
464
+ }
465
+
466
+ // ---------------------------------------------------------------------------
467
+ // Body fields table (for store/update endpoints)
468
+ // ---------------------------------------------------------------------------
469
+
470
+ private buildBodyFieldsTable(
471
+ fields: { name: string; type: string; required: boolean; description: string }[]
472
+ ): string {
473
+ const rows = fields.map(f => {
474
+ const req = f.required
475
+ ? '<span class="text-rose-500 text-[0.625rem] font-semibold">required</span>'
476
+ : '<span class="text-zinc-400 text-[0.625rem]">optional</span>'
477
+ return ` <tr class="border-b border-zinc-100 last:border-0">
478
+ <td class="py-2 pl-4 pr-3 font-mono text-xs text-emerald-600 whitespace-nowrap">${f.name}</td>
479
+ <td class="py-2 pr-3 text-xs text-zinc-500">${f.type}</td>
480
+ <td class="py-2 pr-3">${req}</td>
481
+ <td class="py-2 pr-4 text-xs text-zinc-400">${f.description}</td>
482
+ </tr>`
483
+ })
484
+
485
+ return ` <div>
486
+ <p class="text-xs font-semibold text-zinc-700 mb-2">Request body</p>
487
+ <div class="rounded-md border border-zinc-200 overflow-hidden">
488
+ <table class="w-full text-sm">
489
+ <tbody>
490
+ ${rows.join('\n')}
491
+ </tbody>
492
+ </table>
493
+ </div>
494
+ </div>`
495
+ }
496
+
497
+ // ---------------------------------------------------------------------------
498
+ // Example request
499
+ // ---------------------------------------------------------------------------
500
+
501
+ private buildExampleRequest(
502
+ method: string,
503
+ path: string,
504
+ action: string,
505
+ schema: SchemaDefinition,
506
+ table: TableDefinition
507
+ ): string {
508
+ const hasBody = action === 'store' || action === 'update'
509
+
510
+ if (!hasBody) {
511
+ return ` <div>
512
+ <p class="text-xs font-semibold text-zinc-700 mb-2">Example request</p>
513
+ <div class="rounded-md bg-zinc-900 p-3 overflow-x-auto">
514
+ <pre><code class="text-zinc-100 font-mono">curl -X ${method} \\
515
+ https://&lt;domain&gt;${this.escapeHtml(path)} \\
516
+ -H "Authorization: Bearer &lt;token&gt;"</code></pre>
517
+ </div>
518
+ </div>`
519
+ }
520
+
521
+ const payload = this.buildJsonPayload(action, schema, table)
522
+
523
+ return ` <div>
524
+ <p class="text-xs font-semibold text-zinc-700 mb-2">Example request</p>
525
+ <div class="rounded-md bg-zinc-900 p-3 overflow-x-auto">
526
+ <pre><code class="text-zinc-100 font-mono">curl -X ${method} \\
527
+ https://&lt;domain&gt;${this.escapeHtml(path)} \\
528
+ -H "Authorization: Bearer &lt;token&gt;" \\
529
+ -H "Content-Type: application/json" \\
530
+ -d '${this.escapeHtml(payload)}'</code></pre>
531
+ </div>
532
+ </div>`
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // Example response
537
+ // ---------------------------------------------------------------------------
538
+
539
+ private buildExampleResponse(
540
+ action: string,
541
+ schema: SchemaDefinition,
542
+ table: TableDefinition
543
+ ): string {
544
+ const { statusCode, responseBody } = this.sampleResponse(action, schema, table)
545
+
546
+ return ` <div>
547
+ <p class="text-xs font-semibold text-zinc-700 mb-2">Example response &mdash; <span class="text-emerald-600">${statusCode}</span></p>
548
+ <div class="rounded-md bg-zinc-900 p-3 overflow-x-auto">
549
+ <pre><code class="text-zinc-100 font-mono">${this.escapeHtml(responseBody)}</code></pre>
550
+ </div>
551
+ </div>`
552
+ }
553
+
554
+ // ---------------------------------------------------------------------------
555
+ // Action metadata
556
+ // ---------------------------------------------------------------------------
557
+
558
+ private actionMeta(
559
+ action: string,
560
+ schema: SchemaDefinition
561
+ ): { method: string; pathPattern: string; description: string } {
562
+ const isDependent = DEPENDENT_ARCHETYPES.has(schema.archetype) && !!schema.parent
563
+ const isConfig = schema.archetype === Archetype.Configuration
564
+ const isEvent = schema.archetype === Archetype.Event
565
+ const basePath = this.buildRoutePath(schema)
566
+
567
+ switch (action) {
568
+ case 'index':
569
+ return {
570
+ method: 'GET',
571
+ pathPattern: basePath,
572
+ description: isDependent
573
+ ? `List all ${this.displayNamePlural(schema.name)} under the parent.`
574
+ : `List all ${this.displayNamePlural(schema.name)}.`,
575
+ }
576
+ case 'show':
577
+ return {
578
+ method: 'GET',
579
+ pathPattern: isConfig ? basePath : `${basePath}/:id`,
580
+ description: isConfig
581
+ ? `Retrieve the ${this.displayName(schema.name)} for the parent.`
582
+ : `Retrieve a single ${this.displayName(schema.name)} by ID.`,
583
+ }
584
+ case 'store':
585
+ return {
586
+ method: 'POST',
587
+ pathPattern: basePath,
588
+ description: isEvent
589
+ ? `Append a new ${this.displayName(schema.name)} event.`
590
+ : `Create a new ${this.displayName(schema.name)}.`,
591
+ }
592
+ case 'update':
593
+ return {
594
+ method: 'PUT',
595
+ pathPattern: isConfig ? basePath : `${basePath}/:id`,
596
+ description: isConfig
597
+ ? `Create or update the ${this.displayName(schema.name)} for the parent.`
598
+ : `Update an existing ${this.displayName(schema.name)}.`,
599
+ }
600
+ case 'destroy':
601
+ return {
602
+ method: 'DELETE',
603
+ pathPattern: isConfig ? basePath : `${basePath}/:id`,
604
+ description: isConfig
605
+ ? `Reset the ${this.displayName(schema.name)} to defaults.`
606
+ : `Delete a ${this.displayName(schema.name)}.`,
607
+ }
608
+ default:
609
+ return { method: 'GET', pathPattern: basePath, description: '' }
610
+ }
611
+ }
612
+
613
+ // ---------------------------------------------------------------------------
614
+ // Body fields for an endpoint
615
+ // ---------------------------------------------------------------------------
616
+
617
+ private endpointBodyFields(
618
+ action: string,
619
+ schema: SchemaDefinition,
620
+ table: TableDefinition
621
+ ): { name: string; type: string; required: boolean; description: string }[] {
622
+ if (action !== 'store' && action !== 'update') return []
623
+
624
+ const isStore = action === 'store'
625
+ const fields: { name: string; type: string; required: boolean; description: string }[] = []
626
+
627
+ const parentFkCol = schema.parent
628
+ ? `${toSnakeCase(schema.parent)}_${toSnakeCase(this.findSchemaPK(schema.parent))}`
629
+ : null
630
+
631
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
632
+ if (fieldDef.primaryKey) continue
633
+ if (fieldDef.references) {
634
+ const refPK = this.findSchemaPK(fieldDef.references)
635
+ const fkColName = `${toSnakeCase(fieldName)}_${toSnakeCase(refPK)}`
636
+ if (parentFkCol && fkColName === parentFkCol) continue
637
+ if (SYSTEM_COLUMNS.has(fkColName)) continue
638
+
639
+ fields.push({
640
+ name: toCamelCase(fkColName),
641
+ type: 'string',
642
+ required: isStore && fieldDef.required,
643
+ description: `ID of the referenced ${this.displayName(fieldDef.references)}`,
644
+ })
645
+ continue
646
+ }
647
+
648
+ const colName = toSnakeCase(fieldName)
649
+ if (SYSTEM_COLUMNS.has(colName)) continue
650
+ if (parentFkCol && colName === parentFkCol) continue
651
+
652
+ fields.push({
653
+ name: toCamelCase(fieldName),
654
+ type: this.fieldTypeLabel(fieldDef),
655
+ required: isStore && fieldDef.required,
656
+ description: this.validatorSummary(fieldDef),
657
+ })
658
+ }
659
+
660
+ return fields
661
+ }
662
+
663
+ // ---------------------------------------------------------------------------
664
+ // JSON payloads
665
+ // ---------------------------------------------------------------------------
666
+
667
+ private buildJsonPayload(
668
+ action: string,
669
+ schema: SchemaDefinition,
670
+ table: TableDefinition
671
+ ): string {
672
+ const entries: Record<string, unknown> = {}
673
+
674
+ const parentFkCol = schema.parent
675
+ ? `${toSnakeCase(schema.parent)}_${toSnakeCase(this.findSchemaPK(schema.parent))}`
676
+ : null
677
+
678
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
679
+ if (fieldDef.primaryKey) continue
680
+ if (fieldDef.references) {
681
+ const refPK = this.findSchemaPK(fieldDef.references)
682
+ const fkColName = `${toSnakeCase(fieldName)}_${toSnakeCase(refPK)}`
683
+ if (parentFkCol && fkColName === parentFkCol) continue
684
+ if (SYSTEM_COLUMNS.has(fkColName)) continue
685
+ entries[toCamelCase(fkColName)] = '<uuid>'
686
+ continue
687
+ }
688
+
689
+ const colName = toSnakeCase(fieldName)
690
+ if (SYSTEM_COLUMNS.has(colName)) continue
691
+ if (parentFkCol && colName === parentFkCol) continue
692
+ if (fieldDef.sensitive) continue
693
+
694
+ const camelName = toCamelCase(fieldName)
695
+ entries[camelName] = this.sampleValueLiteral(fieldName, fieldDef)
696
+ }
697
+
698
+ return JSON.stringify(entries, null, 2)
699
+ }
700
+
701
+ private sampleResponse(
702
+ action: string,
703
+ schema: SchemaDefinition,
704
+ table: TableDefinition
705
+ ): { statusCode: number; responseBody: string } {
706
+ if (action === 'destroy') {
707
+ return { statusCode: 200, responseBody: JSON.stringify({ ok: true }, null, 2) }
708
+ }
709
+
710
+ const obj: Record<string, unknown> = {}
711
+ const pkName = this.findSchemaPK(schema.name)
712
+ obj[toCamelCase(pkName)] = this.getPkSample(schema)
713
+
714
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
715
+ if (fieldDef.primaryKey) continue
716
+ const colName = toSnakeCase(fieldName)
717
+ if (colName === 'deleted_at') continue
718
+
719
+ if (fieldDef.references) {
720
+ const refPK = this.findSchemaPK(fieldDef.references)
721
+ const fkColName = `${toSnakeCase(fieldName)}_${toSnakeCase(refPK)}`
722
+ obj[toCamelCase(fkColName)] = '<uuid>'
723
+ continue
724
+ }
725
+
726
+ const camelName = toCamelCase(fieldName)
727
+ if (fieldDef.sensitive) {
728
+ obj[camelName] = '[REDACTED]'
729
+ continue
730
+ }
731
+
732
+ if (colName === 'created_at' || colName === 'updated_at') {
733
+ obj[camelName] = '2025-01-15T10:30:00.000Z'
734
+ continue
735
+ }
736
+
737
+ obj[camelName] = this.sampleValueLiteral(fieldName, fieldDef)
738
+ }
739
+
740
+ const statusCode = action === 'store' ? 201 : 200
741
+ const body =
742
+ action === 'index'
743
+ ? JSON.stringify([obj], null, 2)
744
+ : JSON.stringify(obj, null, 2)
745
+
746
+ return { statusCode, responseBody: body }
747
+ }
748
+
749
+ // ---------------------------------------------------------------------------
750
+ // Route path building (shared with route_generator)
751
+ // ---------------------------------------------------------------------------
752
+
753
+ private buildRoutePath(schema: SchemaDefinition): string {
754
+ const isDependent = DEPENDENT_ARCHETYPES.has(schema.archetype)
755
+ const prefix = this.apiConfig.routing === ApiRouting.Prefix ? this.apiConfig.prefix : ''
756
+
757
+ if (!isDependent || !schema.parent) {
758
+ return `${prefix}/${toRouteSegment(schema.name)}`
759
+ }
760
+
761
+ const parentSegment = toRouteSegment(schema.parent)
762
+ const childSegment = toChildSegment(schema.name, schema.parent)
763
+
764
+ return `${prefix}/${parentSegment}/:parentId/${childSegment}`
765
+ }
766
+
767
+ // ---------------------------------------------------------------------------
768
+ // Helpers
769
+ // ---------------------------------------------------------------------------
770
+
771
+ private displayName(name: string): string {
772
+ return toPascalCase(name).replace(/([A-Z])/g, ' $1').trim()
773
+ }
774
+
775
+ private displayNamePlural(name: string): string {
776
+ const display = this.displayName(name).toLowerCase()
777
+ const lastWord = display.split(' ').pop() ?? display
778
+ const pluralLast = pluralize(lastWord)
779
+ const words = display.split(' ')
780
+ words[words.length - 1] = pluralLast
781
+ return words.join(' ')
782
+ }
783
+
784
+ private findSchemaPK(schemaName: string): string {
785
+ const schema = this.schemaMap.get(schemaName)
786
+ if (!schema) return 'id'
787
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
788
+ if (fieldDef.primaryKey) return fieldName
789
+ }
790
+ return 'id'
791
+ }
792
+
793
+ private getPkSample(schema: SchemaDefinition): string {
794
+ for (const [, fieldDef] of Object.entries(schema.fields)) {
795
+ if (fieldDef.primaryKey) {
796
+ if (String(fieldDef.pgType) === 'uuid') return 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
797
+ return '1'
798
+ }
799
+ }
800
+ return '1'
801
+ }
802
+
803
+ private fieldTypeLabel(fieldDef: FieldDefinition): string {
804
+ if (isCustomType(fieldDef.pgType)) {
805
+ return `enum(${toPascalCase((fieldDef.pgType as PostgreSQLCustomType).name)})`
806
+ }
807
+ if (fieldDef.enumValues?.length) {
808
+ return `enum(${fieldDef.enumValues.join(' | ')})`
809
+ }
810
+ const pgType = String(fieldDef.pgType)
811
+ switch (pgType) {
812
+ case 'varchar':
813
+ case 'character_varying':
814
+ case 'char':
815
+ case 'character':
816
+ case 'text':
817
+ return fieldDef.length ? `string(${fieldDef.length})` : 'string'
818
+ case 'uuid':
819
+ return 'uuid'
820
+ case 'integer':
821
+ case 'smallint':
822
+ case 'serial':
823
+ case 'smallserial':
824
+ return 'integer'
825
+ case 'bigint':
826
+ case 'bigserial':
827
+ return 'bigint'
828
+ case 'real':
829
+ case 'double_precision':
830
+ case 'decimal':
831
+ case 'numeric':
832
+ case 'money':
833
+ return fieldDef.precision
834
+ ? `decimal(${fieldDef.precision},${fieldDef.scale ?? 0})`
835
+ : 'number'
836
+ case 'boolean':
837
+ return 'boolean'
838
+ case 'json':
839
+ case 'jsonb':
840
+ return 'json'
841
+ case 'timestamp':
842
+ case 'timestamptz':
843
+ case 'timestamp_with_time_zone':
844
+ return 'datetime'
845
+ case 'date':
846
+ return 'date'
847
+ case 'time':
848
+ case 'timetz':
849
+ return 'time'
850
+ default:
851
+ return pgType
852
+ }
853
+ }
854
+
855
+ private validatorSummary(fieldDef: FieldDefinition): string {
856
+ const parts: string[] = []
857
+ if (fieldDef.unique) parts.push('unique')
858
+ if (fieldDef.length) parts.push(`max ${fieldDef.length} chars`)
859
+ for (const v of fieldDef.validators) {
860
+ switch (v.type) {
861
+ case 'min':
862
+ parts.push(`min: ${v.params?.value ?? 0}`)
863
+ break
864
+ case 'max':
865
+ parts.push(`max: ${v.params?.value ?? 0}`)
866
+ break
867
+ case 'email':
868
+ parts.push('email format')
869
+ break
870
+ case 'url':
871
+ parts.push('URL format')
872
+ break
873
+ case 'regex':
874
+ parts.push('pattern')
875
+ break
876
+ }
877
+ }
878
+ if (fieldDef.enumValues?.length) {
879
+ parts.push(`one of: ${fieldDef.enumValues.join(', ')}`)
880
+ }
881
+ return parts.join(' &middot; ')
882
+ }
883
+
884
+ private sampleValueLiteral(fieldName: string, fieldDef: FieldDefinition): unknown {
885
+ if (fieldDef.enumValues?.length) return fieldDef.enumValues[0]!
886
+
887
+ const pgType = String(fieldDef.pgType)
888
+ switch (pgType) {
889
+ case 'uuid':
890
+ return 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
891
+ case 'boolean':
892
+ return true
893
+ case 'integer':
894
+ case 'smallint':
895
+ case 'serial':
896
+ case 'smallserial':
897
+ return 42
898
+ case 'bigint':
899
+ case 'bigserial':
900
+ return 42
901
+ case 'real':
902
+ case 'double_precision':
903
+ case 'decimal':
904
+ case 'numeric':
905
+ case 'money':
906
+ return 9.99
907
+ case 'json':
908
+ case 'jsonb':
909
+ return { key: 'value' }
910
+ case 'varchar':
911
+ case 'character_varying':
912
+ case 'char':
913
+ case 'character':
914
+ case 'text':
915
+ default: {
916
+ const snake = toSnakeCase(fieldName)
917
+ if (snake === 'email' || snake.endsWith('_email')) return 'user@example.com'
918
+ if (snake === 'url' || snake.endsWith('_url') || snake === 'website' || snake === 'homepage')
919
+ return 'https://example.com'
920
+ const label = toPascalCase(fieldName).replace(/([A-Z])/g, ' $1').trim()
921
+ return `Sample ${label}`
922
+ }
923
+ }
924
+ }
925
+
926
+ private archetypeBadgeColor(archetype: Archetype): string {
927
+ switch (archetype) {
928
+ case Archetype.Entity:
929
+ return 'bg-sky-100 text-sky-700'
930
+ case Archetype.Contribution:
931
+ return 'bg-violet-100 text-violet-700'
932
+ case Archetype.Reference:
933
+ return 'bg-zinc-100 text-zinc-700'
934
+ case Archetype.Attribute:
935
+ return 'bg-teal-100 text-teal-700'
936
+ case Archetype.Component:
937
+ return 'bg-indigo-100 text-indigo-700'
938
+ case Archetype.Event:
939
+ return 'bg-amber-100 text-amber-700'
940
+ case Archetype.Configuration:
941
+ return 'bg-rose-100 text-rose-700'
942
+ case Archetype.Association:
943
+ return 'bg-zinc-100 text-zinc-700'
944
+ }
945
+ }
946
+
947
+ private methodBadgeColor(method: string): string {
948
+ switch (method) {
949
+ case 'GET':
950
+ return 'bg-emerald-500/10 text-emerald-600'
951
+ case 'POST':
952
+ return 'bg-sky-500/10 text-sky-600'
953
+ case 'PUT':
954
+ case 'PATCH':
955
+ return 'bg-amber-500/10 text-amber-600'
956
+ case 'DELETE':
957
+ return 'bg-rose-500/10 text-rose-600'
958
+ default:
959
+ return 'bg-zinc-500/10 text-zinc-600'
960
+ }
961
+ }
962
+
963
+ private escapeHtml(str: string): string {
964
+ return str
965
+ .replace(/&/g, '&amp;')
966
+ .replace(/</g, '&lt;')
967
+ .replace(/>/g, '&gt;')
968
+ .replace(/"/g, '&quot;')
969
+ }
970
+ }
971
+
972
+ function isCustomType(pgType: unknown): pgType is PostgreSQLCustomType {
973
+ return typeof pgType === 'object' && pgType !== null && (pgType as any).type === 'custom'
974
+ }