@strav/database 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 (39) hide show
  1. package/package.json +46 -0
  2. package/src/database/database.ts +181 -0
  3. package/src/database/domain/context.ts +84 -0
  4. package/src/database/domain/index.ts +17 -0
  5. package/src/database/domain/manager.ts +274 -0
  6. package/src/database/domain/wrapper.ts +105 -0
  7. package/src/database/index.ts +32 -0
  8. package/src/database/introspector.ts +446 -0
  9. package/src/database/migration/differ.ts +308 -0
  10. package/src/database/migration/file_generator.ts +125 -0
  11. package/src/database/migration/index.ts +18 -0
  12. package/src/database/migration/runner.ts +145 -0
  13. package/src/database/migration/sql_generator.ts +378 -0
  14. package/src/database/migration/tracker.ts +86 -0
  15. package/src/database/migration/types.ts +189 -0
  16. package/src/database/query_builder.ts +1034 -0
  17. package/src/database/seeder.ts +31 -0
  18. package/src/helpers/identity.ts +12 -0
  19. package/src/helpers/index.ts +1 -0
  20. package/src/index.ts +5 -0
  21. package/src/orm/base_model.ts +427 -0
  22. package/src/orm/decorators.ts +290 -0
  23. package/src/orm/index.ts +3 -0
  24. package/src/providers/database_provider.ts +25 -0
  25. package/src/providers/index.ts +1 -0
  26. package/src/schema/database_representation.ts +124 -0
  27. package/src/schema/define_association.ts +60 -0
  28. package/src/schema/define_schema.ts +46 -0
  29. package/src/schema/domain_discovery.ts +83 -0
  30. package/src/schema/field_builder.ts +160 -0
  31. package/src/schema/field_definition.ts +69 -0
  32. package/src/schema/index.ts +22 -0
  33. package/src/schema/naming.ts +19 -0
  34. package/src/schema/postgres.ts +109 -0
  35. package/src/schema/registry.ts +187 -0
  36. package/src/schema/representation_builder.ts +482 -0
  37. package/src/schema/type_builder.ts +115 -0
  38. package/src/schema/types.ts +35 -0
  39. package/tsconfig.json +5 -0
@@ -0,0 +1,482 @@
1
+ import { Archetype } from './types'
2
+ import type { SchemaDefinition } from './types'
3
+ import type { FieldDefinition } from './field_definition'
4
+ import type { PostgreSQLCustomType, PostgreSQLType } from './postgres'
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'
16
+ import { toSnakeCase, serialToIntegerType } from './naming'
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 columns for dependent archetypes.
144
+ */
145
+ private addParentFK(
146
+ schema: SchemaDefinition,
147
+ columns: ColumnDefinition[],
148
+ foreignKeys: ForeignKeyConstraint[],
149
+ indexes: IndexDefinition[]
150
+ ): void {
151
+ if (!schema.parents?.length || !PARENT_FK_ARCHETYPES.has(schema.archetype)) return
152
+
153
+ for (const parentName of schema.parents) {
154
+ const parentSchema = this.schemas.get(parentName)
155
+ if (!parentSchema) continue
156
+
157
+ const parentPK = this.findPrimaryKey(parentSchema)
158
+ const fkColName = `${toSnakeCase(parentName)}_${toSnakeCase(parentPK.name)}`
159
+ const fkColType = serialToIntegerType(parentPK.pgType)
160
+
161
+ columns.push({
162
+ name: fkColName,
163
+ pgType: fkColType,
164
+ notNull: true,
165
+ unique: false,
166
+ primaryKey: false,
167
+ autoIncrement: false,
168
+ index: true,
169
+ sensitive: false,
170
+ isArray: false,
171
+ arrayDimensions: 1,
172
+ })
173
+
174
+ foreignKeys.push({
175
+ columns: [fkColName],
176
+ referencedTable: toSnakeCase(parentName),
177
+ referencedColumns: [toSnakeCase(parentPK.name)],
178
+ onDelete: 'CASCADE',
179
+ onUpdate: 'CASCADE',
180
+ })
181
+
182
+ indexes.push({ columns: [fkColName], unique: false })
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Add FK columns for both sides of an association.
188
+ */
189
+ private addAssociationFKs(
190
+ schema: SchemaDefinition,
191
+ columns: ColumnDefinition[],
192
+ foreignKeys: ForeignKeyConstraint[],
193
+ uniqueConstraints: UniqueConstraint[],
194
+ indexes: IndexDefinition[]
195
+ ): void {
196
+ if (schema.archetype !== Archetype.Association || !schema.associates) return
197
+
198
+ const fkColNames: string[] = []
199
+
200
+ for (const entityName of schema.associates) {
201
+ const entitySchema = this.schemas.get(entityName)
202
+ if (!entitySchema) continue
203
+
204
+ const entityPK = this.findPrimaryKey(entitySchema)
205
+ const fkColName = `${toSnakeCase(entityName)}_${toSnakeCase(entityPK.name)}`
206
+ const fkColType = serialToIntegerType(entityPK.pgType)
207
+
208
+ fkColNames.push(fkColName)
209
+
210
+ columns.push({
211
+ name: fkColName,
212
+ pgType: fkColType,
213
+ notNull: true,
214
+ unique: false,
215
+ primaryKey: false,
216
+ autoIncrement: false,
217
+ index: true,
218
+ sensitive: false,
219
+ isArray: false,
220
+ arrayDimensions: 1,
221
+ })
222
+
223
+ foreignKeys.push({
224
+ columns: [fkColName],
225
+ referencedTable: toSnakeCase(entityName),
226
+ referencedColumns: [toSnakeCase(entityPK.name)],
227
+ onDelete: 'CASCADE',
228
+ onUpdate: 'CASCADE',
229
+ })
230
+
231
+ indexes.push({ columns: [fkColName], unique: false })
232
+ }
233
+
234
+ // Composite unique on the FK pair (also add the backing index PostgreSQL creates)
235
+ if (fkColNames.length === 2) {
236
+ uniqueConstraints.push({ columns: fkColNames })
237
+ indexes.push({ columns: fkColNames, unique: true })
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Add user-defined fields. Reference fields are resolved to proper FK columns.
243
+ */
244
+ private addUserFields(
245
+ schema: SchemaDefinition,
246
+ columns: ColumnDefinition[],
247
+ foreignKeys: ForeignKeyConstraint[],
248
+ indexes: IndexDefinition[]
249
+ ): void {
250
+ const associateSet = new Set(schema.associates ?? [])
251
+
252
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
253
+ // Skip PK fields — already handled
254
+ if (fieldDef.primaryKey) continue
255
+
256
+ if (fieldDef.references) {
257
+ // Skip reference fields that duplicate an association FK
258
+ if (associateSet.has(fieldDef.references)) continue
259
+
260
+ this.addReferenceColumn(fieldName, fieldDef, columns, foreignKeys, indexes)
261
+ } else {
262
+ columns.push(this.fieldToColumn(toSnakeCase(fieldName), fieldDef))
263
+
264
+ if (fieldDef.index) {
265
+ indexes.push({ columns: [toSnakeCase(fieldName)], unique: false })
266
+ }
267
+ if (fieldDef.unique) {
268
+ indexes.push({ columns: [toSnakeCase(fieldName)], unique: true })
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Resolve a reference field into a proper FK column.
276
+ */
277
+ private addReferenceColumn(
278
+ fieldName: string,
279
+ fieldDef: FieldDefinition,
280
+ columns: ColumnDefinition[],
281
+ foreignKeys: ForeignKeyConstraint[],
282
+ indexes: IndexDefinition[]
283
+ ): void {
284
+ const refSchema = this.schemas.get(fieldDef.references!)
285
+ if (!refSchema) return
286
+
287
+ const refPK = this.findPrimaryKey(refSchema)
288
+ const fkColName = `${toSnakeCase(fieldName)}_${toSnakeCase(refPK.name)}`
289
+ const fkColType = serialToIntegerType(refPK.pgType)
290
+
291
+ columns.push({
292
+ name: fkColName,
293
+ pgType: fkColType,
294
+ notNull: fieldDef.required,
295
+ unique: fieldDef.unique,
296
+ primaryKey: false,
297
+ autoIncrement: false,
298
+ index: true,
299
+ sensitive: fieldDef.sensitive,
300
+ isArray: false,
301
+ arrayDimensions: 1,
302
+ })
303
+
304
+ foreignKeys.push({
305
+ columns: [fkColName],
306
+ referencedTable: toSnakeCase(fieldDef.references!),
307
+ referencedColumns: [toSnakeCase(refPK.name)],
308
+ onDelete: fieldDef.required ? 'RESTRICT' : 'SET NULL',
309
+ onUpdate: 'CASCADE',
310
+ })
311
+
312
+ indexes.push({ columns: [fkColName], unique: false })
313
+ }
314
+
315
+ /**
316
+ * Add timestamp columns per archetype rules, skipping any already present.
317
+ */
318
+ private addTimestamps(schema: SchemaDefinition, columns: ColumnDefinition[]): void {
319
+ const rules = TIMESTAMP_RULES[schema.archetype]
320
+ if (!rules) return
321
+
322
+ const existing = new Set(columns.map(c => c.name))
323
+
324
+ if (rules.created_at && !existing.has('created_at')) {
325
+ columns.push(this.makeTimestampColumn('created_at', true))
326
+ }
327
+ if (rules.updated_at && !existing.has('updated_at')) {
328
+ columns.push(this.makeTimestampColumn('updated_at', true))
329
+ }
330
+ if (rules.deleted_at && !existing.has('deleted_at')) {
331
+ columns.push(this.makeTimestampColumn('deleted_at', false))
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Assign default values to NOT NULL columns that lack one.
337
+ * FK columns are excluded — they must never receive automatic defaults.
338
+ */
339
+ private applyNotNullDefaults(
340
+ columns: ColumnDefinition[],
341
+ foreignKeys: ForeignKeyConstraint[]
342
+ ): void {
343
+ const fkColumns = new Set(foreignKeys.flatMap(fk => fk.columns))
344
+
345
+ for (const col of columns) {
346
+ if (!col.notNull) continue
347
+ if (col.defaultValue !== undefined) continue
348
+ if (col.autoIncrement) continue
349
+ if (fkColumns.has(col.name)) continue
350
+
351
+ const pgType = typeof col.pgType === 'string' ? col.pgType : null
352
+ if (!pgType) continue
353
+
354
+ const def = this.inferDefault(pgType)
355
+ if (def) col.defaultValue = def
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Infer a sensible default for a given PostgreSQL type.
361
+ */
362
+ private inferDefault(pgType: string): DefaultValue | undefined {
363
+ switch (pgType) {
364
+ case 'uuid':
365
+ return { kind: 'expression', sql: 'gen_random_uuid()' }
366
+ case 'timestamp':
367
+ case 'timestamptz':
368
+ return { kind: 'expression', sql: 'CURRENT_TIMESTAMP' }
369
+ case 'boolean':
370
+ return { kind: 'literal', value: false }
371
+ case 'smallint':
372
+ case 'integer':
373
+ case 'bigint':
374
+ case 'real':
375
+ case 'double_precision':
376
+ case 'decimal':
377
+ case 'numeric':
378
+ case 'money':
379
+ return { kind: 'literal', value: 0 }
380
+ case 'varchar':
381
+ case 'character_varying':
382
+ case 'char':
383
+ case 'character':
384
+ case 'text':
385
+ return { kind: 'literal', value: '' }
386
+ case 'json':
387
+ case 'jsonb':
388
+ return { kind: 'literal', value: '{}' }
389
+ default:
390
+ return undefined
391
+ }
392
+ }
393
+
394
+ // --- Helpers ---
395
+
396
+ /**
397
+ * Find the primary key of a schema. Falls back to the auto-generated default.
398
+ */
399
+ private findPrimaryKey(schema: SchemaDefinition): PKInfo {
400
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
401
+ if (fieldDef.primaryKey) {
402
+ return { name: fieldName, pgType: fieldDef.pgType }
403
+ }
404
+ }
405
+ return { name: 'id', pgType: 'serial' }
406
+ }
407
+
408
+ /**
409
+ * Convert a FieldDefinition to a ColumnDefinition.
410
+ */
411
+ private fieldToColumn(name: string, fieldDef: FieldDefinition): ColumnDefinition {
412
+ return {
413
+ name,
414
+ pgType: fieldDef.pgType,
415
+ notNull: fieldDef.required || fieldDef.primaryKey,
416
+ defaultValue:
417
+ fieldDef.defaultValue !== undefined
418
+ ? { kind: 'literal', value: fieldDef.defaultValue as string | number | boolean | null }
419
+ : undefined,
420
+ unique: fieldDef.unique,
421
+ primaryKey: fieldDef.primaryKey,
422
+ autoIncrement: isSerial(fieldDef.pgType),
423
+ index: fieldDef.index,
424
+ sensitive: fieldDef.sensitive,
425
+ isArray: fieldDef.isArray,
426
+ arrayDimensions: fieldDef.arrayDimensions,
427
+ length: fieldDef.length,
428
+ precision: fieldDef.precision,
429
+ scale: fieldDef.scale,
430
+ isUlid: fieldDef.isUlid,
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Create a timestamp column definition.
436
+ */
437
+ private makeTimestampColumn(name: string, notNull: boolean): ColumnDefinition {
438
+ return {
439
+ name,
440
+ pgType: 'timestamptz',
441
+ notNull,
442
+ defaultValue: notNull ? { kind: 'expression', sql: 'CURRENT_TIMESTAMP' } : undefined,
443
+ unique: false,
444
+ primaryKey: false,
445
+ autoIncrement: false,
446
+ index: false,
447
+ sensitive: false,
448
+ isArray: false,
449
+ arrayDimensions: 1,
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Collect all enum definitions from all schemas, deduped by name.
455
+ */
456
+ private collectEnums(): EnumDefinition[] {
457
+ const enums: EnumDefinition[] = []
458
+ const seen = new Set<string>()
459
+
460
+ for (const schema of this.schemas.values()) {
461
+ for (const fieldDef of Object.values(schema.fields)) {
462
+ if (isCustomType(fieldDef.pgType) && fieldDef.pgType.values?.length) {
463
+ const enumName = fieldDef.pgType.name
464
+ if (enumName && !seen.has(enumName)) {
465
+ seen.add(enumName)
466
+ enums.push({ name: enumName, values: fieldDef.pgType.values })
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ return enums
473
+ }
474
+ }
475
+
476
+ function isCustomType(pgType: unknown): pgType is PostgreSQLCustomType {
477
+ return typeof pgType === 'object' && pgType !== null && (pgType as any).type === 'custom'
478
+ }
479
+
480
+ function isSerial(pgType: PostgreSQLType): boolean {
481
+ return pgType === 'serial' || pgType === 'bigserial' || pgType === 'smallserial'
482
+ }
@@ -0,0 +1,115 @@
1
+ import type { PostgreSQLCustomType } from './postgres'
2
+ import FieldBuilder from './field_builder'
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/database/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
+ /** ULID (Universally Unique Lexicographically Sortable Identifier) stored as char(26). */
107
+ ulid: () => {
108
+ const builder = new FieldBuilder('char', { length: 26 })
109
+ // Mark this as a ULID type internally
110
+ ;(builder as any)._isUlid = true
111
+ return builder
112
+ },
113
+ }
114
+
115
+ export default t
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Schema DSL Types - Complete PostgreSQL type support
3
+ */
4
+ import type { FieldDefinition } from './field_definition'
5
+ import type FieldBuilder from './field_builder'
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
+ parents?: 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
+ parents?: string[]
32
+ associates?: string[]
33
+ as?: Record<string, string>
34
+ fields: Record<string, FieldDefinition>
35
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src/**/*.ts"],
4
+ "exclude": ["node_modules", "tests"]
5
+ }