@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,155 @@
1
+ import type { PostgreSQLType } from './postgres.ts'
2
+ import type { FieldDefinition, FieldValidator } from './field_definition.ts'
3
+
4
+ /** Initial options that the type builder can pass at construction time. */
5
+ interface FieldBuilderOptions {
6
+ length?: number
7
+ precision?: number
8
+ scale?: number
9
+ references?: string
10
+ enumValues?: string[]
11
+ }
12
+
13
+ /**
14
+ * Fluent builder for a single field definition.
15
+ *
16
+ * Created by the `t` type builder; collects modifiers, validators,
17
+ * and type parameters, then exposes the result as a {@link FieldDefinition}.
18
+ *
19
+ * @example
20
+ * t.varchar(255).email().unique().required()
21
+ */
22
+ export default class FieldBuilder {
23
+ private def: FieldDefinition
24
+
25
+ constructor(pgType: PostgreSQLType, options?: FieldBuilderOptions) {
26
+ this.def = {
27
+ pgType,
28
+ required: false,
29
+ nullable: false,
30
+ unique: false,
31
+ primaryKey: false,
32
+ index: false,
33
+ sensitive: false,
34
+ isArray: false,
35
+ arrayDimensions: 1,
36
+ length: options?.length,
37
+ precision: options?.precision,
38
+ scale: options?.scale,
39
+ references: options?.references,
40
+ enumValues: options?.enumValues,
41
+ validators: [],
42
+ }
43
+ }
44
+
45
+ // === Modifiers ===
46
+
47
+ /** Mark the field as NOT NULL. */
48
+ required(): this {
49
+ this.def.required = true
50
+ return this
51
+ }
52
+
53
+ /** Mark the field as explicitly nullable. */
54
+ nullable(): this {
55
+ this.def.nullable = true
56
+ return this
57
+ }
58
+
59
+ /** Add a UNIQUE constraint. */
60
+ unique(): this {
61
+ this.def.unique = true
62
+ return this
63
+ }
64
+
65
+ /** Set the column's DEFAULT value. */
66
+ default(value: unknown): this {
67
+ this.def.defaultValue = value
68
+ return this
69
+ }
70
+
71
+ /** Mark the field as a primary key. */
72
+ primaryKey(): this {
73
+ this.def.primaryKey = true
74
+ return this
75
+ }
76
+
77
+ /** Add a database index on this field. */
78
+ index(): this {
79
+ this.def.index = true
80
+ return this
81
+ }
82
+
83
+ /** Mark the field as containing sensitive data. */
84
+ sensitive(): this {
85
+ this.def.sensitive = true
86
+ return this
87
+ }
88
+
89
+ /** Convert this field to a PostgreSQL array type. */
90
+ array(dimensions: number = 1): this {
91
+ this.def.isArray = true
92
+ this.def.arrayDimensions = dimensions
93
+ return this
94
+ }
95
+
96
+ // === Validators ===
97
+
98
+ /** Minimum value/length validator. */
99
+ min(value: number): this {
100
+ this.def.validators.push({ type: 'min', params: { value } })
101
+ return this
102
+ }
103
+
104
+ /** Maximum value/length validator. */
105
+ max(value: number): this {
106
+ this.def.validators.push({ type: 'max', params: { value } })
107
+ return this
108
+ }
109
+
110
+ /** Email format validator. */
111
+ email(): this {
112
+ this.def.validators.push({ type: 'email' })
113
+ return this
114
+ }
115
+
116
+ /** URL format validator. */
117
+ url(): this {
118
+ this.def.validators.push({ type: 'url' })
119
+ return this
120
+ }
121
+
122
+ /** Regex pattern validator. */
123
+ regex(pattern: string | RegExp): this {
124
+ const source = pattern instanceof RegExp ? pattern.source : pattern
125
+ this.def.validators.push({ type: 'regex', params: { pattern: source } })
126
+ return this
127
+ }
128
+
129
+ /** String length validator (exact or range). */
130
+ length(min: number, max?: number): this {
131
+ this.def.validators.push({ type: 'length', params: { min, max } })
132
+ return this
133
+ }
134
+
135
+ /** Numeric precision. Also sets the type parameter for SQL generation. */
136
+ precision(p: number): this {
137
+ this.def.precision = p
138
+ this.def.validators.push({ type: 'precision', params: { value: p } })
139
+ return this
140
+ }
141
+
142
+ /** Numeric scale. Also sets the type parameter for SQL generation. */
143
+ scale(s: number): this {
144
+ this.def.scale = s
145
+ this.def.validators.push({ type: 'scale', params: { value: s } })
146
+ return this
147
+ }
148
+
149
+ // === Accessors ===
150
+
151
+ /** Return the finalized field definition. */
152
+ toDefinition(): FieldDefinition {
153
+ return { ...this.def, validators: [...this.def.validators] }
154
+ }
155
+ }
@@ -0,0 +1,66 @@
1
+ import type { PostgreSQLType } from './postgres.ts'
2
+
3
+ /**
4
+ * A single validation constraint.
5
+ * Code generators read these to produce runtime validation rules.
6
+ */
7
+ export interface FieldValidator {
8
+ type: 'min' | 'max' | 'email' | 'url' | 'regex' | 'length' | 'precision' | 'scale'
9
+ params?: Record<string, unknown>
10
+ }
11
+
12
+ /**
13
+ * Serializable description of a single schema field.
14
+ *
15
+ * Produced by the {@link FieldBuilder} fluent chain, consumed by
16
+ * code generators, migration engines, and validators.
17
+ */
18
+ export interface FieldDefinition {
19
+ /** The resolved PostgreSQL column type (e.g. 'varchar', 'integer', 'jsonb'). */
20
+ pgType: PostgreSQLType
21
+
22
+ /** Whether the field is NOT NULL. */
23
+ required: boolean
24
+
25
+ /** Whether the field explicitly allows NULL. */
26
+ nullable: boolean
27
+
28
+ /** Whether the field has a UNIQUE constraint. */
29
+ unique: boolean
30
+
31
+ /** Default value expression (literal or SQL expression string). */
32
+ defaultValue?: unknown
33
+
34
+ /** Whether the field is a primary key. */
35
+ primaryKey: boolean
36
+
37
+ /** Whether the field should have a database index. */
38
+ index: boolean
39
+
40
+ /** Whether the field contains sensitive data (excluded from logs/serialization). */
41
+ sensitive: boolean
42
+
43
+ /** Whether the field is an array. If true, pgType describes the element type. */
44
+ isArray: boolean
45
+
46
+ /** Array dimensions (1 = one-dimensional, 2 = two-dimensional, etc.). */
47
+ arrayDimensions: number
48
+
49
+ /** Maximum length for varchar/char, or bit length for bit types. */
50
+ length?: number
51
+
52
+ /** Precision for decimal/numeric types. */
53
+ precision?: number
54
+
55
+ /** Scale for decimal/numeric types. */
56
+ scale?: number
57
+
58
+ /** For reference fields: the name of the referenced table/schema. */
59
+ references?: string
60
+
61
+ /** For enum fields: the list of allowed string values. */
62
+ enumValues?: string[]
63
+
64
+ /** Validation constraints collected from the fluent chain. */
65
+ validators: FieldValidator[]
66
+ }
@@ -0,0 +1,21 @@
1
+ export { default as defineSchema } from './define_schema.ts'
2
+ export { default as defineAssociation } from './define_association.ts'
3
+ export { default as t } from './type_builder.ts'
4
+ export { default as FieldBuilder } from './field_builder.ts'
5
+ export { default as SchemaRegistry } from './registry.ts'
6
+ export { default as RepresentationBuilder } from './representation_builder.ts'
7
+ export { toSnakeCase, serialToIntegerType } from './naming.ts'
8
+ export type { FieldDefinition, FieldValidator } from './field_definition.ts'
9
+ export { Archetype } from './types.ts'
10
+ export type { SchemaDefinition, SchemaInput } from './types.ts'
11
+ export type {
12
+ DatabaseRepresentation,
13
+ TableDefinition,
14
+ ColumnDefinition,
15
+ EnumDefinition,
16
+ ForeignKeyConstraint,
17
+ PrimaryKeyConstraint,
18
+ UniqueConstraint,
19
+ IndexDefinition,
20
+ DefaultValue,
21
+ } from './database_representation.ts'
@@ -0,0 +1,19 @@
1
+ import type { PostgreSQLType } from './postgres.ts'
2
+
3
+ export { toSnakeCase } from '../helpers/strings.ts'
4
+
5
+ /**
6
+ * Map a serial/bigserial/smallserial pgType to the corresponding integer type
7
+ * used for foreign key columns. All other types pass through unchanged.
8
+ *
9
+ * @example
10
+ * serialToIntegerType('serial') // 'integer'
11
+ * serialToIntegerType('bigserial') // 'bigint'
12
+ * serialToIntegerType('uuid') // 'uuid'
13
+ */
14
+ export function serialToIntegerType(pgType: PostgreSQLType): PostgreSQLType {
15
+ if (pgType === 'serial') return 'integer'
16
+ if (pgType === 'bigserial') return 'bigint'
17
+ if (pgType === 'smallserial') return 'smallint'
18
+ return pgType
19
+ }
@@ -0,0 +1,109 @@
1
+ // Complete PostgreSQL type system
2
+ export type PostgreSQLType =
3
+ | PostgreSQLNumericTypes
4
+ | PostgreSQLMonetaryTypes
5
+ | PostgreSQLCharacterTypes
6
+ | PostgreSQLBinaryTypes
7
+ | PostgreSQLDateTimeTypes
8
+ | PostgreSQLBooleanTypes
9
+ | PostgreSQLUUIDTypes
10
+ | PostgreSQLNetworkTypes
11
+ | PostgreSQLBitStringTypes
12
+ | PostgreSQLTextSearchTypes
13
+ | PostgreSQLJSONTypes
14
+ | PostgreSQLGeometricTypes
15
+ | PostgreSQLRangeTypes
16
+ | PostgreSQLXMLTypes
17
+ | PostgreSQLArrayType
18
+ | PostgreSQLCustomType
19
+
20
+ /** Integer, arbitrary precision, floating-point, and auto-incrementing types. */
21
+ export type PostgreSQLNumericTypes =
22
+ | 'smallint'
23
+ | 'integer'
24
+ | 'bigint'
25
+ | 'decimal'
26
+ | 'numeric'
27
+ | 'real'
28
+ | 'double_precision'
29
+ | 'smallserial'
30
+ | 'serial'
31
+ | 'bigserial'
32
+
33
+ /** Currency amounts with locale-aware formatting. */
34
+ export type PostgreSQLMonetaryTypes = 'money'
35
+
36
+ /** Fixed-length, variable-length, and unlimited text. */
37
+ export type PostgreSQLCharacterTypes =
38
+ | 'character_varying'
39
+ | 'varchar'
40
+ | 'character'
41
+ | 'char'
42
+ | 'text'
43
+
44
+ /** Raw binary data (byte array). */
45
+ export type PostgreSQLBinaryTypes = 'bytea'
46
+
47
+ /** Dates, times, timestamps, and intervals. */
48
+ export type PostgreSQLDateTimeTypes =
49
+ | 'timestamp'
50
+ | 'timestamptz'
51
+ | 'date'
52
+ | 'time'
53
+ | 'timetz'
54
+ | 'interval'
55
+
56
+ /** True/false logical value. */
57
+ export type PostgreSQLBooleanTypes = 'boolean'
58
+
59
+ /** RFC 4122 universally unique identifiers. */
60
+ export type PostgreSQLUUIDTypes = 'uuid'
61
+
62
+ /** IPv4/IPv6 addresses, CIDR blocks, and MAC addresses. */
63
+ export type PostgreSQLNetworkTypes = 'inet' | 'cidr' | 'macaddr' | 'macaddr8'
64
+
65
+ /** Fixed-length and variable-length bit strings. */
66
+ export type PostgreSQLBitStringTypes = 'bit' | 'bit_varying'
67
+
68
+ /** Full-text search document and query representations. */
69
+ export type PostgreSQLTextSearchTypes = 'tsvector' | 'tsquery'
70
+
71
+ /** JSON data stored as text or decomposed binary. */
72
+ export type PostgreSQLJSONTypes = 'json' | 'jsonb'
73
+
74
+ /** XML document data. */
75
+ export type PostgreSQLXMLTypes = 'xml'
76
+
77
+ /** 2D geometric primitives: points, lines, segments, boxes, paths, polygons, and circles. */
78
+ export type PostgreSQLGeometricTypes =
79
+ | 'point'
80
+ | 'line'
81
+ | 'lseg'
82
+ | 'box'
83
+ | 'path'
84
+ | 'polygon'
85
+ | 'circle'
86
+
87
+ /** Ranges over integer, numeric, timestamp, and date values. */
88
+ export type PostgreSQLRangeTypes =
89
+ | 'int4range'
90
+ | 'int8range'
91
+ | 'numrange'
92
+ | 'tsrange'
93
+ | 'tstzrange'
94
+ | 'daterange'
95
+
96
+ /** Typed multi-dimensional array of any non-array PostgreSQL type. */
97
+ export interface PostgreSQLArrayType {
98
+ type: 'array'
99
+ element: Exclude<PostgreSQLType, PostgreSQLArrayType>
100
+ dimensions?: number
101
+ }
102
+
103
+ /** User-defined type, enum, or domain. */
104
+ export interface PostgreSQLCustomType {
105
+ type: 'custom'
106
+ name: string // Custom type name or enum name
107
+ definition?: string // For inline enum definitions
108
+ values?: string[] // For enum types - the actual values array
109
+ }
@@ -0,0 +1,157 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+ import type { SchemaDefinition } from './types.ts'
4
+ import type { DatabaseRepresentation } from './database_representation.ts'
5
+ import RepresentationBuilder from './representation_builder.ts'
6
+
7
+ /**
8
+ * Discovers, stores, validates, and provides dependency-ordered
9
+ * access to all schema definitions in the application.
10
+ *
11
+ * @example
12
+ * const registry = new SchemaRegistry()
13
+ * await registry.discover('database/schemas')
14
+ * registry.validate()
15
+ * const ordered = registry.resolve() // topologically sorted
16
+ */
17
+ export default class SchemaRegistry {
18
+ private schemas = new Map<string, SchemaDefinition>()
19
+
20
+ /** Register a single schema definition. */
21
+ register(schema: SchemaDefinition): this {
22
+ if (this.schemas.has(schema.name)) {
23
+ throw new Error(`Schema "${schema.name}" is already registered`)
24
+ }
25
+ this.schemas.set(schema.name, schema)
26
+ return this
27
+ }
28
+
29
+ /** Retrieve a schema by name. */
30
+ get(name: string): SchemaDefinition | undefined {
31
+ return this.schemas.get(name)
32
+ }
33
+
34
+ /** Check whether a schema with the given name exists. */
35
+ has(name: string): boolean {
36
+ return this.schemas.has(name)
37
+ }
38
+
39
+ /** Return all registered schemas as an array. */
40
+ all(): SchemaDefinition[] {
41
+ return Array.from(this.schemas.values())
42
+ }
43
+
44
+ /**
45
+ * Scan a directory for schema files and register all discovered schemas.
46
+ * Each `.ts` file must default-export a {@link SchemaDefinition}.
47
+ */
48
+ async discover(schemasPath: string): Promise<void> {
49
+ const absPath = resolve(schemasPath)
50
+ let files: string[]
51
+ try {
52
+ files = readdirSync(absPath)
53
+ } catch {
54
+ throw new Error(`Schemas directory not found: ${schemasPath}`)
55
+ }
56
+
57
+ for (const file of files) {
58
+ if (!file.endsWith('.ts')) continue
59
+ const filePath = join(absPath, file)
60
+ const mod = await import(filePath)
61
+ const schema: SchemaDefinition = mod.default
62
+ if (!schema || !schema.name) {
63
+ throw new Error(`Schema file "${file}" does not export a valid SchemaDefinition`)
64
+ }
65
+ this.register(schema)
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Validate all schemas: check that every reference and parent
71
+ * points to a registered schema.
72
+ */
73
+ validate(): void {
74
+ for (const schema of this.schemas.values()) {
75
+ if (schema.parent && !this.schemas.has(schema.parent)) {
76
+ throw new Error(
77
+ `Schema "${schema.name}" references parent "${schema.parent}" which is not registered`
78
+ )
79
+ }
80
+
81
+ if (schema.associates) {
82
+ for (const assoc of schema.associates) {
83
+ if (!this.schemas.has(assoc)) {
84
+ throw new Error(
85
+ `Schema "${schema.name}" associates with "${assoc}" which is not registered`
86
+ )
87
+ }
88
+ }
89
+ }
90
+
91
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
92
+ if (fieldDef.references && !this.schemas.has(fieldDef.references)) {
93
+ throw new Error(
94
+ `Schema "${schema.name}" field "${fieldName}" references "${fieldDef.references}" which is not registered`
95
+ )
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Return schemas in dependency order (topological sort).
103
+ * Schemas with no dependencies come first.
104
+ * Throws if a circular dependency is detected.
105
+ */
106
+ resolve(): SchemaDefinition[] {
107
+ const visited = new Set<string>()
108
+ const visiting = new Set<string>()
109
+ const sorted: SchemaDefinition[] = []
110
+
111
+ const visit = (name: string, path: string[]) => {
112
+ if (visited.has(name)) return
113
+ if (visiting.has(name)) {
114
+ throw new Error(`Circular dependency detected: ${[...path, name].join(' -> ')}`)
115
+ }
116
+
117
+ visiting.add(name)
118
+ const schema = this.schemas.get(name)!
119
+ for (const dep of this.getDependencies(schema)) {
120
+ visit(dep, [...path, name])
121
+ }
122
+ visiting.delete(name)
123
+ visited.add(name)
124
+ sorted.push(schema)
125
+ }
126
+
127
+ for (const name of this.schemas.keys()) {
128
+ visit(name, [])
129
+ }
130
+
131
+ return sorted
132
+ }
133
+
134
+ /**
135
+ * Generate the {@link DatabaseRepresentation} from all registered schemas.
136
+ * Must be called after {@link validate} to ensure all references are resolvable.
137
+ */
138
+ buildRepresentation(): DatabaseRepresentation {
139
+ const ordered = this.resolve()
140
+ return new RepresentationBuilder(ordered).build()
141
+ }
142
+
143
+ /** Collect all schema names that the given schema depends on. */
144
+ private getDependencies(schema: SchemaDefinition): string[] {
145
+ const deps = new Set<string>()
146
+
147
+ if (schema.parent) deps.add(schema.parent)
148
+ if (schema.associates) {
149
+ for (const assoc of schema.associates) deps.add(assoc)
150
+ }
151
+ for (const fieldDef of Object.values(schema.fields)) {
152
+ if (fieldDef.references) deps.add(fieldDef.references)
153
+ }
154
+
155
+ return Array.from(deps)
156
+ }
157
+ }