@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,479 @@
1
+ import { Archetype } from './types.ts'
2
+ import type { SchemaDefinition } from './types.ts'
3
+ import type { FieldDefinition } from './field_definition.ts'
4
+ import type { PostgreSQLCustomType, PostgreSQLType } from './postgres.ts'
5
+ import type {
6
+ DatabaseRepresentation,
7
+ TableDefinition,
8
+ ColumnDefinition,
9
+ EnumDefinition,
10
+ ForeignKeyConstraint,
11
+ PrimaryKeyConstraint,
12
+ UniqueConstraint,
13
+ IndexDefinition,
14
+ DefaultValue,
15
+ } from './database_representation.ts'
16
+ import { toSnakeCase, serialToIntegerType } from './naming.ts'
17
+
18
+ /** Timestamp columns each archetype receives automatically. */
19
+ const TIMESTAMP_RULES: Record<
20
+ Archetype,
21
+ { created_at: boolean; updated_at: boolean; deleted_at: boolean }
22
+ > = {
23
+ [Archetype.Entity]: { created_at: true, updated_at: true, deleted_at: true },
24
+ [Archetype.Component]: { created_at: true, updated_at: true, deleted_at: false },
25
+ [Archetype.Attribute]: { created_at: true, updated_at: true, deleted_at: true },
26
+ [Archetype.Association]: { created_at: true, updated_at: true, deleted_at: false },
27
+ [Archetype.Event]: { created_at: true, updated_at: false, deleted_at: false },
28
+ [Archetype.Reference]: { created_at: true, updated_at: true, deleted_at: false },
29
+ [Archetype.Configuration]: { created_at: true, updated_at: true, deleted_at: false },
30
+ [Archetype.Contribution]: { created_at: true, updated_at: true, deleted_at: true },
31
+ }
32
+
33
+ /** Archetypes that have a parent FK (dependent archetypes except association). */
34
+ const PARENT_FK_ARCHETYPES: Set<Archetype> = new Set([
35
+ Archetype.Component,
36
+ Archetype.Attribute,
37
+ Archetype.Event,
38
+ Archetype.Configuration,
39
+ Archetype.Contribution,
40
+ ])
41
+
42
+ /** Resolved primary key info for a schema. */
43
+ interface PKInfo {
44
+ name: string
45
+ pgType: PostgreSQLType
46
+ }
47
+
48
+ /**
49
+ * Transforms a set of {@link SchemaDefinition}s into a {@link DatabaseRepresentation}.
50
+ *
51
+ * Schemas must be provided in dependency order (from {@link SchemaRegistry.resolve}).
52
+ */
53
+ export default class RepresentationBuilder {
54
+ private schemas: Map<string, SchemaDefinition>
55
+
56
+ constructor(schemas: SchemaDefinition[]) {
57
+ this.schemas = new Map(schemas.map(s => [s.name, s]))
58
+ }
59
+
60
+ build(): DatabaseRepresentation {
61
+ const enums = this.collectEnums()
62
+ const tables: TableDefinition[] = []
63
+
64
+ for (const schema of this.schemas.values()) {
65
+ tables.push(this.buildTable(schema))
66
+ }
67
+
68
+ return { enums, tables }
69
+ }
70
+
71
+ private buildTable(schema: SchemaDefinition): TableDefinition {
72
+ const columns: ColumnDefinition[] = []
73
+ const foreignKeys: ForeignKeyConstraint[] = []
74
+ const uniqueConstraints: UniqueConstraint[] = []
75
+ const indexes: IndexDefinition[] = []
76
+
77
+ // 1. Primary key
78
+ const pk = this.addPrimaryKey(schema, columns)
79
+
80
+ // 2. Parent FK (for dependent archetypes)
81
+ this.addParentFK(schema, columns, foreignKeys, indexes)
82
+
83
+ // 3. Association FKs
84
+ this.addAssociationFKs(schema, columns, foreignKeys, uniqueConstraints, indexes)
85
+
86
+ // 4. User-defined fields (non-reference fields)
87
+ // 5. Reference fields resolved to FK columns
88
+ this.addUserFields(schema, columns, foreignKeys, indexes)
89
+
90
+ // 6. Timestamps
91
+ this.addTimestamps(schema, columns)
92
+
93
+ // 7. NOT NULL defaults (skip FK columns — they must never have defaults)
94
+ this.applyNotNullDefaults(columns, foreignKeys)
95
+
96
+ return {
97
+ name: toSnakeCase(schema.name),
98
+ archetype: schema.archetype,
99
+ columns,
100
+ primaryKey: pk,
101
+ foreignKeys,
102
+ uniqueConstraints,
103
+ indexes,
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Add the primary key column. Returns the PK constraint or null for associations.
109
+ */
110
+ private addPrimaryKey(
111
+ schema: SchemaDefinition,
112
+ columns: ColumnDefinition[]
113
+ ): PrimaryKeyConstraint | null {
114
+ if (schema.archetype === Archetype.Association) return null
115
+
116
+ // Check if developer specified a PK
117
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
118
+ if (fieldDef.primaryKey) {
119
+ const colName = toSnakeCase(fieldName)
120
+ columns.push(this.fieldToColumn(colName, fieldDef))
121
+ return { columns: [colName] }
122
+ }
123
+ }
124
+
125
+ // Auto-add default: id serial
126
+ columns.push({
127
+ name: 'id',
128
+ pgType: 'serial',
129
+ notNull: true,
130
+ unique: true,
131
+ primaryKey: true,
132
+ autoIncrement: true,
133
+ index: false,
134
+ sensitive: false,
135
+ isArray: false,
136
+ arrayDimensions: 1,
137
+ })
138
+
139
+ return { columns: ['id'] }
140
+ }
141
+
142
+ /**
143
+ * Add parent FK column for dependent archetypes.
144
+ */
145
+ private addParentFK(
146
+ schema: SchemaDefinition,
147
+ columns: ColumnDefinition[],
148
+ foreignKeys: ForeignKeyConstraint[],
149
+ indexes: IndexDefinition[]
150
+ ): void {
151
+ if (!schema.parent || !PARENT_FK_ARCHETYPES.has(schema.archetype)) return
152
+
153
+ const parentSchema = this.schemas.get(schema.parent)
154
+ if (!parentSchema) return
155
+
156
+ const parentPK = this.findPrimaryKey(parentSchema)
157
+ const fkColName = `${toSnakeCase(schema.parent)}_${toSnakeCase(parentPK.name)}`
158
+ const fkColType = serialToIntegerType(parentPK.pgType)
159
+
160
+ columns.push({
161
+ name: fkColName,
162
+ pgType: fkColType,
163
+ notNull: true,
164
+ unique: false,
165
+ primaryKey: false,
166
+ autoIncrement: false,
167
+ index: true,
168
+ sensitive: false,
169
+ isArray: false,
170
+ arrayDimensions: 1,
171
+ })
172
+
173
+ foreignKeys.push({
174
+ columns: [fkColName],
175
+ referencedTable: toSnakeCase(schema.parent),
176
+ referencedColumns: [toSnakeCase(parentPK.name)],
177
+ onDelete: 'CASCADE',
178
+ onUpdate: 'CASCADE',
179
+ })
180
+
181
+ indexes.push({ columns: [fkColName], unique: false })
182
+ }
183
+
184
+ /**
185
+ * Add FK columns for both sides of an association.
186
+ */
187
+ private addAssociationFKs(
188
+ schema: SchemaDefinition,
189
+ columns: ColumnDefinition[],
190
+ foreignKeys: ForeignKeyConstraint[],
191
+ uniqueConstraints: UniqueConstraint[],
192
+ indexes: IndexDefinition[]
193
+ ): void {
194
+ if (schema.archetype !== Archetype.Association || !schema.associates) return
195
+
196
+ const fkColNames: string[] = []
197
+
198
+ for (const entityName of schema.associates) {
199
+ const entitySchema = this.schemas.get(entityName)
200
+ if (!entitySchema) continue
201
+
202
+ const entityPK = this.findPrimaryKey(entitySchema)
203
+ const fkColName = `${toSnakeCase(entityName)}_${toSnakeCase(entityPK.name)}`
204
+ const fkColType = serialToIntegerType(entityPK.pgType)
205
+
206
+ fkColNames.push(fkColName)
207
+
208
+ columns.push({
209
+ name: fkColName,
210
+ pgType: fkColType,
211
+ notNull: true,
212
+ unique: false,
213
+ primaryKey: false,
214
+ autoIncrement: false,
215
+ index: true,
216
+ sensitive: false,
217
+ isArray: false,
218
+ arrayDimensions: 1,
219
+ })
220
+
221
+ foreignKeys.push({
222
+ columns: [fkColName],
223
+ referencedTable: toSnakeCase(entityName),
224
+ referencedColumns: [toSnakeCase(entityPK.name)],
225
+ onDelete: 'CASCADE',
226
+ onUpdate: 'CASCADE',
227
+ })
228
+
229
+ indexes.push({ columns: [fkColName], unique: false })
230
+ }
231
+
232
+ // Composite unique on the FK pair (also add the backing index PostgreSQL creates)
233
+ if (fkColNames.length === 2) {
234
+ uniqueConstraints.push({ columns: fkColNames })
235
+ indexes.push({ columns: fkColNames, unique: true })
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Add user-defined fields. Reference fields are resolved to proper FK columns.
241
+ */
242
+ private addUserFields(
243
+ schema: SchemaDefinition,
244
+ columns: ColumnDefinition[],
245
+ foreignKeys: ForeignKeyConstraint[],
246
+ indexes: IndexDefinition[]
247
+ ): void {
248
+ const associateSet = new Set(schema.associates ?? [])
249
+
250
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
251
+ // Skip PK fields — already handled
252
+ if (fieldDef.primaryKey) continue
253
+
254
+ if (fieldDef.references) {
255
+ // Skip reference fields that duplicate an association FK
256
+ if (associateSet.has(fieldDef.references)) continue
257
+
258
+ this.addReferenceColumn(fieldName, fieldDef, columns, foreignKeys, indexes)
259
+ } else {
260
+ columns.push(this.fieldToColumn(toSnakeCase(fieldName), fieldDef))
261
+
262
+ if (fieldDef.index) {
263
+ indexes.push({ columns: [toSnakeCase(fieldName)], unique: false })
264
+ }
265
+ if (fieldDef.unique) {
266
+ indexes.push({ columns: [toSnakeCase(fieldName)], unique: true })
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Resolve a reference field into a proper FK column.
274
+ */
275
+ private addReferenceColumn(
276
+ fieldName: string,
277
+ fieldDef: FieldDefinition,
278
+ columns: ColumnDefinition[],
279
+ foreignKeys: ForeignKeyConstraint[],
280
+ indexes: IndexDefinition[]
281
+ ): void {
282
+ const refSchema = this.schemas.get(fieldDef.references!)
283
+ if (!refSchema) return
284
+
285
+ const refPK = this.findPrimaryKey(refSchema)
286
+ const fkColName = `${toSnakeCase(fieldName)}_${toSnakeCase(refPK.name)}`
287
+ const fkColType = serialToIntegerType(refPK.pgType)
288
+
289
+ columns.push({
290
+ name: fkColName,
291
+ pgType: fkColType,
292
+ notNull: fieldDef.required,
293
+ unique: fieldDef.unique,
294
+ primaryKey: false,
295
+ autoIncrement: false,
296
+ index: true,
297
+ sensitive: fieldDef.sensitive,
298
+ isArray: false,
299
+ arrayDimensions: 1,
300
+ })
301
+
302
+ foreignKeys.push({
303
+ columns: [fkColName],
304
+ referencedTable: toSnakeCase(fieldDef.references!),
305
+ referencedColumns: [toSnakeCase(refPK.name)],
306
+ onDelete: fieldDef.required ? 'RESTRICT' : 'SET NULL',
307
+ onUpdate: 'CASCADE',
308
+ })
309
+
310
+ indexes.push({ columns: [fkColName], unique: false })
311
+ }
312
+
313
+ /**
314
+ * Add timestamp columns per archetype rules, skipping any already present.
315
+ */
316
+ private addTimestamps(schema: SchemaDefinition, columns: ColumnDefinition[]): void {
317
+ const rules = TIMESTAMP_RULES[schema.archetype]
318
+ if (!rules) return
319
+
320
+ const existing = new Set(columns.map(c => c.name))
321
+
322
+ if (rules.created_at && !existing.has('created_at')) {
323
+ columns.push(this.makeTimestampColumn('created_at', true))
324
+ }
325
+ if (rules.updated_at && !existing.has('updated_at')) {
326
+ columns.push(this.makeTimestampColumn('updated_at', true))
327
+ }
328
+ if (rules.deleted_at && !existing.has('deleted_at')) {
329
+ columns.push(this.makeTimestampColumn('deleted_at', false))
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Assign default values to NOT NULL columns that lack one.
335
+ * FK columns are excluded — they must never receive automatic defaults.
336
+ */
337
+ private applyNotNullDefaults(
338
+ columns: ColumnDefinition[],
339
+ foreignKeys: ForeignKeyConstraint[]
340
+ ): void {
341
+ const fkColumns = new Set(foreignKeys.flatMap(fk => fk.columns))
342
+
343
+ for (const col of columns) {
344
+ if (!col.notNull) continue
345
+ if (col.defaultValue !== undefined) continue
346
+ if (col.autoIncrement) continue
347
+ if (fkColumns.has(col.name)) continue
348
+
349
+ const pgType = typeof col.pgType === 'string' ? col.pgType : null
350
+ if (!pgType) continue
351
+
352
+ const def = this.inferDefault(pgType)
353
+ if (def) col.defaultValue = def
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Infer a sensible default for a given PostgreSQL type.
359
+ */
360
+ private inferDefault(pgType: string): DefaultValue | undefined {
361
+ switch (pgType) {
362
+ case 'uuid':
363
+ return { kind: 'expression', sql: 'gen_random_uuid()' }
364
+ case 'timestamp':
365
+ case 'timestamptz':
366
+ return { kind: 'expression', sql: 'CURRENT_TIMESTAMP' }
367
+ case 'boolean':
368
+ return { kind: 'literal', value: false }
369
+ case 'smallint':
370
+ case 'integer':
371
+ case 'bigint':
372
+ case 'real':
373
+ case 'double_precision':
374
+ case 'decimal':
375
+ case 'numeric':
376
+ case 'money':
377
+ return { kind: 'literal', value: 0 }
378
+ case 'varchar':
379
+ case 'character_varying':
380
+ case 'char':
381
+ case 'character':
382
+ case 'text':
383
+ return { kind: 'literal', value: '' }
384
+ case 'json':
385
+ case 'jsonb':
386
+ return { kind: 'literal', value: '{}' }
387
+ default:
388
+ return undefined
389
+ }
390
+ }
391
+
392
+ // --- Helpers ---
393
+
394
+ /**
395
+ * Find the primary key of a schema. Falls back to the auto-generated default.
396
+ */
397
+ private findPrimaryKey(schema: SchemaDefinition): PKInfo {
398
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
399
+ if (fieldDef.primaryKey) {
400
+ return { name: fieldName, pgType: fieldDef.pgType }
401
+ }
402
+ }
403
+ return { name: 'id', pgType: 'serial' }
404
+ }
405
+
406
+ /**
407
+ * Convert a FieldDefinition to a ColumnDefinition.
408
+ */
409
+ private fieldToColumn(name: string, fieldDef: FieldDefinition): ColumnDefinition {
410
+ return {
411
+ name,
412
+ pgType: fieldDef.pgType,
413
+ notNull: fieldDef.required || fieldDef.primaryKey,
414
+ defaultValue:
415
+ fieldDef.defaultValue !== undefined
416
+ ? { kind: 'literal', value: fieldDef.defaultValue as string | number | boolean | null }
417
+ : undefined,
418
+ unique: fieldDef.unique,
419
+ primaryKey: fieldDef.primaryKey,
420
+ autoIncrement: isSerial(fieldDef.pgType),
421
+ index: fieldDef.index,
422
+ sensitive: fieldDef.sensitive,
423
+ isArray: fieldDef.isArray,
424
+ arrayDimensions: fieldDef.arrayDimensions,
425
+ length: fieldDef.length,
426
+ precision: fieldDef.precision,
427
+ scale: fieldDef.scale,
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Create a timestamp column definition.
433
+ */
434
+ private makeTimestampColumn(name: string, notNull: boolean): ColumnDefinition {
435
+ return {
436
+ name,
437
+ pgType: 'timestamptz',
438
+ notNull,
439
+ defaultValue: notNull ? { kind: 'expression', sql: 'CURRENT_TIMESTAMP' } : undefined,
440
+ unique: false,
441
+ primaryKey: false,
442
+ autoIncrement: false,
443
+ index: false,
444
+ sensitive: false,
445
+ isArray: false,
446
+ arrayDimensions: 1,
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Collect all enum definitions from all schemas, deduped by name.
452
+ */
453
+ private collectEnums(): EnumDefinition[] {
454
+ const enums: EnumDefinition[] = []
455
+ const seen = new Set<string>()
456
+
457
+ for (const schema of this.schemas.values()) {
458
+ for (const fieldDef of Object.values(schema.fields)) {
459
+ if (isCustomType(fieldDef.pgType) && fieldDef.pgType.values?.length) {
460
+ const enumName = fieldDef.pgType.name
461
+ if (enumName && !seen.has(enumName)) {
462
+ seen.add(enumName)
463
+ enums.push({ name: enumName, values: fieldDef.pgType.values })
464
+ }
465
+ }
466
+ }
467
+ }
468
+
469
+ return enums
470
+ }
471
+ }
472
+
473
+ function isCustomType(pgType: unknown): pgType is PostgreSQLCustomType {
474
+ return typeof pgType === 'object' && pgType !== null && (pgType as any).type === 'custom'
475
+ }
476
+
477
+ function isSerial(pgType: PostgreSQLType): boolean {
478
+ return pgType === 'serial' || pgType === 'bigserial' || pgType === 'smallserial'
479
+ }
@@ -0,0 +1,107 @@
1
+ import type { PostgreSQLCustomType } from './postgres.ts'
2
+ import FieldBuilder from './field_builder.ts'
3
+
4
+ /**
5
+ * Type builder for schema field definitions.
6
+ *
7
+ * Provides factory methods for every PostgreSQL data type.
8
+ * Each method returns a {@link FieldBuilder} with the correct pgType pre-configured.
9
+ *
10
+ * @example
11
+ * import { t } from '@stravigor/core/schema'
12
+ * t.varchar(255).email().unique().required()
13
+ * t.integer().default(0)
14
+ * t.jsonb().nullable()
15
+ */
16
+ const t = {
17
+ // --- Numeric Types ---
18
+ smallint: () => new FieldBuilder('smallint'),
19
+ integer: () => new FieldBuilder('integer'),
20
+ bigint: () => new FieldBuilder('bigint'),
21
+ decimal: (precision?: number, scale?: number) =>
22
+ new FieldBuilder('decimal', { precision, scale }),
23
+ numeric: (precision?: number, scale?: number) =>
24
+ new FieldBuilder('numeric', { precision, scale }),
25
+ real: () => new FieldBuilder('real'),
26
+ double: () => new FieldBuilder('double_precision'),
27
+ smallserial: () => new FieldBuilder('smallserial'),
28
+ serial: () => new FieldBuilder('serial'),
29
+ bigserial: () => new FieldBuilder('bigserial'),
30
+
31
+ // --- Monetary ---
32
+ money: () => new FieldBuilder('money'),
33
+
34
+ // --- Character Types ---
35
+ varchar: (length?: number) => new FieldBuilder('varchar', { length }),
36
+ /** Alias for {@link varchar}. */
37
+ string: (length?: number) => new FieldBuilder('varchar', { length }),
38
+ char: (length?: number) => new FieldBuilder('char', { length }),
39
+ text: () => new FieldBuilder('text'),
40
+
41
+ // --- Binary ---
42
+ bytea: () => new FieldBuilder('bytea'),
43
+
44
+ // --- Date/Time ---
45
+ timestamp: () => new FieldBuilder('timestamp'),
46
+ timestamptz: () => new FieldBuilder('timestamptz'),
47
+ date: () => new FieldBuilder('date'),
48
+ time: () => new FieldBuilder('time'),
49
+ timetz: () => new FieldBuilder('timetz'),
50
+ interval: () => new FieldBuilder('interval'),
51
+
52
+ // --- Boolean ---
53
+ boolean: () => new FieldBuilder('boolean'),
54
+
55
+ // --- UUID ---
56
+ uuid: () => new FieldBuilder('uuid'),
57
+
58
+ // --- Geometric Types ---
59
+ point: () => new FieldBuilder('point'),
60
+ line: () => new FieldBuilder('line'),
61
+ lseg: () => new FieldBuilder('lseg'),
62
+ box: () => new FieldBuilder('box'),
63
+ path: () => new FieldBuilder('path'),
64
+ polygon: () => new FieldBuilder('polygon'),
65
+ circle: () => new FieldBuilder('circle'),
66
+
67
+ // --- Network Address Types ---
68
+ inet: () => new FieldBuilder('inet'),
69
+ cidr: () => new FieldBuilder('cidr'),
70
+ macaddr: () => new FieldBuilder('macaddr'),
71
+ macaddr8: () => new FieldBuilder('macaddr8'),
72
+
73
+ // --- Bit String Types ---
74
+ bit: (length?: number) => new FieldBuilder('bit', { length }),
75
+ varbit: (length?: number) => new FieldBuilder('bit_varying', { length }),
76
+
77
+ // --- Text Search Types ---
78
+ tsvector: () => new FieldBuilder('tsvector'),
79
+ tsquery: () => new FieldBuilder('tsquery'),
80
+
81
+ // --- JSON Types ---
82
+ json: () => new FieldBuilder('json'),
83
+ jsonb: () => new FieldBuilder('jsonb'),
84
+
85
+ // --- Range Types ---
86
+ int4range: () => new FieldBuilder('int4range'),
87
+ int8range: () => new FieldBuilder('int8range'),
88
+ numrange: () => new FieldBuilder('numrange'),
89
+ tsrange: () => new FieldBuilder('tsrange'),
90
+ tstzrange: () => new FieldBuilder('tstzrange'),
91
+ daterange: () => new FieldBuilder('daterange'),
92
+
93
+ // --- XML ---
94
+ xml: () => new FieldBuilder('xml'),
95
+
96
+ // --- Enum ---
97
+ /** Create a PostgreSQL enum type with the given allowed values. */
98
+ enum: (values: string[]) => {
99
+ const customType: PostgreSQLCustomType = { type: 'custom', name: '', values }
100
+ return new FieldBuilder(customType, { enumValues: values })
101
+ },
102
+
103
+ /** Foreign key reference to another schema. Column type defaults to UUID. */
104
+ reference: (table: string) => new FieldBuilder('uuid', { references: table }),
105
+ }
106
+
107
+ export default t
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Schema DSL Types - Complete PostgreSQL type support
3
+ */
4
+ import type { FieldDefinition } from './field_definition.ts'
5
+ import type FieldBuilder from './field_builder.ts'
6
+
7
+ export enum Archetype {
8
+ Entity = 'entity',
9
+ Component = 'component',
10
+ Attribute = 'attribute',
11
+ Association = 'association',
12
+ Event = 'event',
13
+ Reference = 'reference',
14
+ Configuration = 'configuration',
15
+ Contribution = 'contribution',
16
+ }
17
+
18
+ /** The input shape that users pass to {@link defineSchema}. */
19
+ export interface SchemaInput {
20
+ archetype?: Archetype
21
+ parent?: string
22
+ associates?: string[]
23
+ as?: Record<string, string>
24
+ fields: Record<string, FieldBuilder>
25
+ }
26
+
27
+ /** The resolved schema stored in the registry. */
28
+ export interface SchemaDefinition {
29
+ name: string
30
+ archetype: Archetype
31
+ parent?: string
32
+ associates?: string[]
33
+ as?: Record<string, string>
34
+ fields: Record<string, FieldDefinition>
35
+ }
@@ -0,0 +1,4 @@
1
+ export { default as Session } from './session.ts'
2
+ export { default as SessionManager } from './session_manager.ts'
3
+ export { session } from './middleware.ts'
4
+ export type { SessionConfig } from './session_manager.ts'
@@ -0,0 +1,46 @@
1
+ import type { Middleware } from '../http/middleware.ts'
2
+ import { withCookie } from '../http/cookie.ts'
3
+ import Session from './session.ts'
4
+ import SessionManager from './session_manager.ts'
5
+
6
+ /**
7
+ * Session middleware — attaches a Session to every request.
8
+ *
9
+ * 1. Reads the session cookie and loads the session from DB
10
+ * 2. Creates a new anonymous session if absent or expired
11
+ * 3. Ages flash data so previous-request flash is readable
12
+ * 4. Sets `ctx.get('session')` and `ctx.get('csrfToken')`
13
+ * 5. After the handler: saves dirty data and refreshes the cookie
14
+ *
15
+ * @example
16
+ * import { session } from '@stravigor/core/session'
17
+ * router.use(session())
18
+ */
19
+ export function session(): Middleware {
20
+ return async (ctx, next) => {
21
+ let sess = await Session.fromRequest(ctx)
22
+
23
+ if (!sess || sess.isExpired()) {
24
+ sess = Session.create(ctx)
25
+ }
26
+
27
+ sess.ageFlash()
28
+
29
+ ctx.set('session', sess)
30
+ ctx.set('csrfToken', sess.csrfToken)
31
+
32
+ const response = await next()
33
+
34
+ await sess.save()
35
+
36
+ // Refresh cookie (sliding expiration)
37
+ const cfg = SessionManager.config
38
+ return withCookie(response, cfg.cookie, sess.id, {
39
+ httpOnly: cfg.httpOnly,
40
+ secure: cfg.secure,
41
+ sameSite: cfg.sameSite,
42
+ maxAge: cfg.lifetime * 60,
43
+ path: '/',
44
+ })
45
+ }
46
+ }