@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,116 @@
1
+ import Scheduler from './scheduler.ts'
2
+ import type { Schedule } from './schedule.ts'
3
+
4
+ export interface RunnerOptions {
5
+ /** Timezone for schedule evaluation. @default 'UTC' */
6
+ timezone?: string
7
+ }
8
+
9
+ /**
10
+ * Long-running scheduler loop.
11
+ *
12
+ * Aligns to minute boundaries (standard cron behaviour), checks which
13
+ * tasks are due, and executes them concurrently. Supports graceful
14
+ * shutdown via SIGINT/SIGTERM and per-task overlap prevention.
15
+ *
16
+ * @example
17
+ * const runner = new SchedulerRunner()
18
+ * await runner.start() // blocks until runner.stop()
19
+ */
20
+ export default class SchedulerRunner {
21
+ private running = false
22
+ private active = new Set<string>()
23
+
24
+ /** Start the scheduler loop. Blocks until stop() is called. */
25
+ async start(): Promise<void> {
26
+ this.running = true
27
+
28
+ const onSignal = () => this.stop()
29
+ process.on('SIGINT', onSignal)
30
+ process.on('SIGTERM', onSignal)
31
+
32
+ try {
33
+ while (this.running) {
34
+ await this.sleepUntilNextMinute()
35
+ if (!this.running) break
36
+
37
+ const now = new Date()
38
+ const due = Scheduler.due(now)
39
+
40
+ if (due.length > 0) {
41
+ await this.runAll(due)
42
+ }
43
+ }
44
+
45
+ // Wait for active tasks to finish before exiting
46
+ if (this.active.size > 0) {
47
+ await this.waitForActive()
48
+ }
49
+ } finally {
50
+ process.off('SIGINT', onSignal)
51
+ process.off('SIGTERM', onSignal)
52
+ }
53
+ }
54
+
55
+ /** Signal the runner to stop after the current tick completes. */
56
+ stop(): void {
57
+ this.running = false
58
+ }
59
+
60
+ /** Number of tasks currently executing. */
61
+ get activeCount(): number {
62
+ return this.active.size
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Internal
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /** Sleep until the start of the next minute (XX:XX:00.000). */
70
+ private async sleepUntilNextMinute(): Promise<void> {
71
+ const now = Date.now()
72
+ const nextMinute = Math.ceil(now / 60_000) * 60_000
73
+ const delay = nextMinute - now
74
+
75
+ // Minimum 1s, maximum 60s — avoid sleeping 0ms on exact boundaries
76
+ const ms = Math.max(1_000, Math.min(delay, 60_000))
77
+ await Bun.sleep(ms)
78
+ }
79
+
80
+ /** Execute all due tasks concurrently. */
81
+ private async runAll(tasks: Schedule[]): Promise<void> {
82
+ const promises: Promise<void>[] = []
83
+
84
+ for (const task of tasks) {
85
+ if (task.preventsOverlap && this.active.has(task.name)) {
86
+ continue
87
+ }
88
+
89
+ promises.push(this.runTask(task))
90
+ }
91
+
92
+ await Promise.allSettled(promises)
93
+ }
94
+
95
+ /** Execute a single task with overlap tracking and error handling. */
96
+ private async runTask(task: Schedule): Promise<void> {
97
+ this.active.add(task.name)
98
+ try {
99
+ await task.handler()
100
+ } catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error)
102
+ console.error(`[scheduler] Task "${task.name}" failed: ${message}`)
103
+ } finally {
104
+ this.active.delete(task.name)
105
+ }
106
+ }
107
+
108
+ /** Wait for all active tasks to complete (for graceful shutdown). */
109
+ private async waitForActive(): Promise<void> {
110
+ const maxWait = 30_000
111
+ const start = Date.now()
112
+ while (this.active.size > 0 && Date.now() - start < maxWait) {
113
+ await Bun.sleep(100)
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,183 @@
1
+ import { parseCron, cronMatches } from './cron.ts'
2
+ import type { CronExpression } from './cron.ts'
3
+
4
+ export type TaskHandler = () => void | Promise<void>
5
+
6
+ const DAY_NAMES: Record<string, number> = {
7
+ sunday: 0, sun: 0,
8
+ monday: 1, mon: 1,
9
+ tuesday: 2, tue: 2,
10
+ wednesday: 3, wed: 3,
11
+ thursday: 4, thu: 4,
12
+ friday: 5, fri: 5,
13
+ saturday: 6, sat: 6,
14
+ }
15
+
16
+ /**
17
+ * A scheduled task definition with a fluent configuration API.
18
+ *
19
+ * @example
20
+ * new Schedule('cleanup', handler).dailyAt('02:30').withoutOverlapping()
21
+ */
22
+ export class Schedule {
23
+ readonly name: string
24
+ readonly handler: TaskHandler
25
+
26
+ private _cron: CronExpression | null = null
27
+ private _noOverlap = false
28
+
29
+ constructor(name: string, handler: TaskHandler) {
30
+ this.name = name
31
+ this.handler = handler
32
+ }
33
+
34
+ // ── Raw cron ──────────────────────────────────────────────────────────────
35
+
36
+ /** Set a raw 5-field cron expression. */
37
+ cron(expression: string): this {
38
+ this._cron = parseCron(expression)
39
+ return this
40
+ }
41
+
42
+ // ── Minute-based ──────────────────────────────────────────────────────────
43
+
44
+ /** Run every minute. */
45
+ everyMinute(): this {
46
+ return this.cron('* * * * *')
47
+ }
48
+
49
+ /** Run every 2 minutes. */
50
+ everyTwoMinutes(): this {
51
+ return this.cron('*/2 * * * *')
52
+ }
53
+
54
+ /** Run every 5 minutes. */
55
+ everyFiveMinutes(): this {
56
+ return this.cron('*/5 * * * *')
57
+ }
58
+
59
+ /** Run every 10 minutes. */
60
+ everyTenMinutes(): this {
61
+ return this.cron('*/10 * * * *')
62
+ }
63
+
64
+ /** Run every 15 minutes. */
65
+ everyFifteenMinutes(): this {
66
+ return this.cron('*/15 * * * *')
67
+ }
68
+
69
+ /** Run every 30 minutes. */
70
+ everyThirtyMinutes(): this {
71
+ return this.cron('*/30 * * * *')
72
+ }
73
+
74
+ // ── Hourly ────────────────────────────────────────────────────────────────
75
+
76
+ /** Run once per hour at minute 0. */
77
+ hourly(): this {
78
+ return this.cron('0 * * * *')
79
+ }
80
+
81
+ /** Run once per hour at the given minute. */
82
+ hourlyAt(minute: number): this {
83
+ return this.cron(`${minute} * * * *`)
84
+ }
85
+
86
+ // ── Daily ─────────────────────────────────────────────────────────────────
87
+
88
+ /** Run once per day at midnight. */
89
+ daily(): this {
90
+ return this.cron('0 0 * * *')
91
+ }
92
+
93
+ /** Run once per day at the given time (HH:MM). */
94
+ dailyAt(time: string): this {
95
+ const [hour, minute] = parseTime(time)
96
+ return this.cron(`${minute} ${hour} * * *`)
97
+ }
98
+
99
+ /** Run twice per day at the given hours (minute 0). */
100
+ twiceDaily(hour1: number, hour2: number): this {
101
+ return this.cron(`0 ${hour1},${hour2} * * *`)
102
+ }
103
+
104
+ // ── Weekly ────────────────────────────────────────────────────────────────
105
+
106
+ /** Run once per week on Sunday at midnight. */
107
+ weekly(): this {
108
+ return this.cron('0 0 * * 0')
109
+ }
110
+
111
+ /** Run once per week on the given day and optional time. */
112
+ weeklyOn(day: string | number, time?: string): this {
113
+ const dow = typeof day === 'string' ? dayToNumber(day) : day
114
+ const [hour, minute] = time ? parseTime(time) : [0, 0]
115
+ return this.cron(`${minute} ${hour} * * ${dow}`)
116
+ }
117
+
118
+ // ── Monthly ───────────────────────────────────────────────────────────────
119
+
120
+ /** Run once per month on the 1st at midnight. */
121
+ monthly(): this {
122
+ return this.cron('0 0 1 * *')
123
+ }
124
+
125
+ /** Run once per month on the given day and optional time. */
126
+ monthlyOn(day: number, time?: string): this {
127
+ const [hour, minute] = time ? parseTime(time) : [0, 0]
128
+ return this.cron(`${minute} ${hour} ${day} * *`)
129
+ }
130
+
131
+ // ── Options ───────────────────────────────────────────────────────────────
132
+
133
+ /** Prevent overlapping runs within this process. */
134
+ withoutOverlapping(): this {
135
+ this._noOverlap = true
136
+ return this
137
+ }
138
+
139
+ // ── Internal ──────────────────────────────────────────────────────────────
140
+
141
+ /** Check if this task is due at the given Date (evaluated in UTC). */
142
+ isDue(now: Date): boolean {
143
+ if (!this._cron) return false
144
+ return cronMatches(this._cron, now)
145
+ }
146
+
147
+ /** Whether overlap prevention is enabled. */
148
+ get preventsOverlap(): boolean {
149
+ return this._noOverlap
150
+ }
151
+
152
+ /** The parsed cron expression (for testing/debugging). */
153
+ get expression(): CronExpression | null {
154
+ return this._cron
155
+ }
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Helpers
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function parseTime(time: string): [number, number] {
163
+ const parts = time.split(':')
164
+ if (parts.length !== 2) {
165
+ throw new Error(`Invalid time format "${time}": expected HH:MM`)
166
+ }
167
+ const hour = parseInt(parts[0]!, 10)
168
+ const minute = parseInt(parts[1]!, 10)
169
+ if (isNaN(hour) || isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
170
+ throw new Error(`Invalid time "${time}": hour must be 0–23, minute must be 0–59`)
171
+ }
172
+ return [hour, minute]
173
+ }
174
+
175
+ function dayToNumber(day: string): number {
176
+ const n = DAY_NAMES[day.toLowerCase()]
177
+ if (n === undefined) {
178
+ throw new Error(
179
+ `Invalid day name "${day}": expected one of ${Object.keys(DAY_NAMES).filter((_, i) => i % 2 === 0).join(', ')}`
180
+ )
181
+ }
182
+ return n
183
+ }
@@ -0,0 +1,47 @@
1
+ import { Schedule } from './schedule.ts'
2
+ import type { TaskHandler } from './schedule.ts'
3
+
4
+ /**
5
+ * Static task registry for periodic jobs.
6
+ *
7
+ * No DI, no database — tasks are registered in code and evaluated
8
+ * in-memory by the {@link SchedulerRunner}.
9
+ *
10
+ * @example
11
+ * Scheduler.task('cleanup:sessions', async () => {
12
+ * await db.sql`DELETE FROM "_strav_sessions" WHERE "expires_at" < NOW()`
13
+ * }).hourly()
14
+ *
15
+ * Scheduler.task('reports:daily', () => generateDailyReport()).dailyAt('02:00')
16
+ */
17
+ export default class Scheduler {
18
+ private static _tasks: Schedule[] = []
19
+
20
+ /**
21
+ * Register a periodic task. Returns the {@link Schedule} for fluent configuration.
22
+ *
23
+ * @example
24
+ * Scheduler.task('prune-cache', () => cache.flush()).everyFifteenMinutes()
25
+ */
26
+ static task(name: string, handler: TaskHandler): Schedule {
27
+ const schedule = new Schedule(name, handler)
28
+ Scheduler._tasks.push(schedule)
29
+ return schedule
30
+ }
31
+
32
+ /** All registered tasks. */
33
+ static get tasks(): readonly Schedule[] {
34
+ return Scheduler._tasks
35
+ }
36
+
37
+ /** Return tasks that are due at the given time (defaults to now, UTC). */
38
+ static due(now?: Date): Schedule[] {
39
+ const date = now ?? new Date()
40
+ return Scheduler._tasks.filter(t => t.isDue(date))
41
+ }
42
+
43
+ /** Clear all registered tasks. For testing. */
44
+ static reset(): void {
45
+ Scheduler._tasks = []
46
+ }
47
+ }
@@ -0,0 +1,122 @@
1
+ import type { PostgreSQLType } from './postgres.ts'
2
+ import type { Archetype } from './types.ts'
3
+
4
+ /**
5
+ * Represents the full database schema derived from all registered SchemaDefinitions.
6
+ * Contains all enum types and table definitions in dependency order.
7
+ */
8
+ export interface DatabaseRepresentation {
9
+ /** PostgreSQL enum types that must be created before tables. */
10
+ enums: EnumDefinition[]
11
+ /** Table definitions in dependency order. */
12
+ tables: TableDefinition[]
13
+ }
14
+
15
+ /**
16
+ * A PostgreSQL CREATE TYPE ... AS ENUM definition.
17
+ */
18
+ export interface EnumDefinition {
19
+ /** Enum type name (e.g. 'order_status'). */
20
+ name: string
21
+ /** Allowed values. */
22
+ values: string[]
23
+ }
24
+
25
+ /**
26
+ * A PostgreSQL table definition.
27
+ */
28
+ export interface TableDefinition {
29
+ /** Table name in snake_case, not pluralized. */
30
+ name: string
31
+ /** The archetype this table was derived from (absent when introspected from DB). */
32
+ archetype?: Archetype
33
+ /** Ordered list of columns. */
34
+ columns: ColumnDefinition[]
35
+ /** Primary key constraint, or null for associations. */
36
+ primaryKey: PrimaryKeyConstraint | null
37
+ /** Foreign key constraints. */
38
+ foreignKeys: ForeignKeyConstraint[]
39
+ /** Unique constraints (beyond individual column UNIQUE). */
40
+ uniqueConstraints: UniqueConstraint[]
41
+ /** Index definitions. */
42
+ indexes: IndexDefinition[]
43
+ }
44
+
45
+ /**
46
+ * A single column in a table.
47
+ */
48
+ export interface ColumnDefinition {
49
+ /** Column name in snake_case. */
50
+ name: string
51
+ /** PostgreSQL column type. */
52
+ pgType: PostgreSQLType
53
+ /** Whether the column is NOT NULL. */
54
+ notNull: boolean
55
+ /** Default value (literal or SQL expression). */
56
+ defaultValue?: DefaultValue
57
+ /** Whether the column has a UNIQUE constraint. */
58
+ unique: boolean
59
+ /** Whether the column is part of the primary key. */
60
+ primaryKey: boolean
61
+ /** Whether the column is auto-incrementing (serial). */
62
+ autoIncrement: boolean
63
+ /** Whether the column should be indexed. */
64
+ index: boolean
65
+ /** Whether the column contains sensitive data (app-level, absent when introspected). */
66
+ sensitive?: boolean
67
+ /** Whether this is an array column. */
68
+ isArray: boolean
69
+ /** Array dimensions. */
70
+ arrayDimensions: number
71
+ /** Max length for varchar/char. */
72
+ length?: number
73
+ /** Precision for decimal/numeric. */
74
+ precision?: number
75
+ /** Scale for decimal/numeric. */
76
+ scale?: number
77
+ }
78
+
79
+ /**
80
+ * A default value for a column — either a literal value or a SQL expression.
81
+ */
82
+ export type DefaultValue =
83
+ | { kind: 'literal'; value: string | number | boolean | null }
84
+ | { kind: 'expression'; sql: string }
85
+
86
+ /**
87
+ * A primary key constraint (single or composite).
88
+ */
89
+ export interface PrimaryKeyConstraint {
90
+ columns: string[]
91
+ }
92
+
93
+ /**
94
+ * A foreign key constraint.
95
+ */
96
+ export interface ForeignKeyConstraint {
97
+ /** Column(s) in this table. */
98
+ columns: string[]
99
+ /** Referenced table. */
100
+ referencedTable: string
101
+ /** Referenced column(s). */
102
+ referencedColumns: string[]
103
+ /** ON DELETE behavior. */
104
+ onDelete: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'
105
+ /** ON UPDATE behavior. */
106
+ onUpdate: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'
107
+ }
108
+
109
+ /**
110
+ * A unique constraint across one or more columns.
111
+ */
112
+ export interface UniqueConstraint {
113
+ columns: string[]
114
+ }
115
+
116
+ /**
117
+ * An index definition.
118
+ */
119
+ export interface IndexDefinition {
120
+ columns: string[]
121
+ unique: boolean
122
+ }
@@ -0,0 +1,60 @@
1
+ import { Archetype } from './types.ts'
2
+ import type { SchemaDefinition } from './types.ts'
3
+ import type FieldBuilder from './field_builder.ts'
4
+ import defineSchema from './define_schema.ts'
5
+
6
+ interface AssociationOptions {
7
+ as?: Record<string, string>
8
+ fields?: Record<string, FieldBuilder>
9
+ }
10
+
11
+ /**
12
+ * Define an association (many-to-many junction) schema between two entities.
13
+ *
14
+ * Automatically sets archetype to `'association'`, generates the table name
15
+ * as `{entityA}_{entityB}`, and records the two associates.
16
+ *
17
+ * Only specify additional pivot fields — the FK columns to both entities
18
+ * are injected automatically by the {@link RepresentationBuilder}.
19
+ *
20
+ * @example
21
+ * // With named properties on each entity model:
22
+ * export default defineAssociation(['team', 'user'], {
23
+ * as: { team: 'members', user: 'teams' },
24
+ * fields: {
25
+ * role: t.enum(['admin', 'developer', 'tester']),
26
+ * },
27
+ * })
28
+ *
29
+ * // Legacy shorthand (fields only, no model properties):
30
+ * export default defineAssociation(['team', 'user'], {
31
+ * role: t.enum(['admin', 'developer', 'tester']),
32
+ * })
33
+ */
34
+ export default function defineAssociation(
35
+ entities: [string, string],
36
+ options: AssociationOptions | Record<string, FieldBuilder> = {}
37
+ ): SchemaDefinition {
38
+ const [entityA, entityB] = entities
39
+
40
+ let fields: Record<string, FieldBuilder>
41
+ let as: Record<string, string> | undefined
42
+
43
+ if (isAssociationOptions(options)) {
44
+ fields = options.fields ?? {}
45
+ as = options.as
46
+ } else {
47
+ fields = options
48
+ }
49
+
50
+ return defineSchema(`${entityA}_${entityB}`, {
51
+ archetype: Archetype.Association,
52
+ associates: [entityA, entityB],
53
+ as,
54
+ fields,
55
+ })
56
+ }
57
+
58
+ function isAssociationOptions(obj: unknown): obj is AssociationOptions {
59
+ return typeof obj === 'object' && obj !== null && ('as' in obj || 'fields' in obj)
60
+ }
@@ -0,0 +1,46 @@
1
+ import { Archetype } from './types.ts'
2
+ import type { SchemaInput, SchemaDefinition } from './types.ts'
3
+ import type { FieldDefinition } from './field_definition.ts'
4
+ import type { PostgreSQLCustomType } from './postgres.ts'
5
+
6
+ /**
7
+ * Define a data schema for the application.
8
+ *
9
+ * Resolves all {@link FieldBuilder} instances into {@link FieldDefinition}s,
10
+ * assigns proper enum names, and returns a {@link SchemaDefinition}.
11
+ *
12
+ * @example
13
+ * export default defineSchema('user', {
14
+ * archetype: Archetype.Entity,
15
+ * fields: {
16
+ * email: t.varchar().email().unique().required(),
17
+ * role: t.enum(['user', 'admin']).default('user'),
18
+ * },
19
+ * })
20
+ */
21
+ export default function defineSchema(name: string, input: SchemaInput): SchemaDefinition {
22
+ const fields: Record<string, FieldDefinition> = {}
23
+
24
+ for (const [fieldName, builder] of Object.entries(input.fields)) {
25
+ const def = builder.toDefinition()
26
+
27
+ if (isCustomType(def.pgType) && def.pgType.values?.length) {
28
+ def.pgType = { ...def.pgType, name: `${name}_${fieldName}` }
29
+ }
30
+
31
+ fields[fieldName] = def
32
+ }
33
+
34
+ return {
35
+ name,
36
+ archetype: input.archetype ?? Archetype.Entity,
37
+ parent: input.parent,
38
+ associates: input.associates,
39
+ as: input.as,
40
+ fields,
41
+ }
42
+ }
43
+
44
+ function isCustomType(pgType: unknown): pgType is PostgreSQLCustomType {
45
+ return typeof pgType === 'object' && pgType !== null && (pgType as any).type === 'custom'
46
+ }