@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.
- package/README.md +45 -0
- package/package.json +83 -0
- package/src/auth/access_token.ts +122 -0
- package/src/auth/auth.ts +86 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/middleware/authenticate.ts +64 -0
- package/src/auth/middleware/csrf.ts +62 -0
- package/src/auth/middleware/guest.ts +46 -0
- package/src/broadcast/broadcast_manager.ts +411 -0
- package/src/broadcast/client.ts +302 -0
- package/src/broadcast/index.ts +58 -0
- package/src/cache/cache_manager.ts +56 -0
- package/src/cache/cache_store.ts +31 -0
- package/src/cache/helpers.ts +74 -0
- package/src/cache/http_cache.ts +109 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/memory_store.ts +63 -0
- package/src/cli/bootstrap.ts +37 -0
- package/src/cli/commands/generate_api.ts +74 -0
- package/src/cli/commands/generate_key.ts +46 -0
- package/src/cli/commands/generate_models.ts +48 -0
- package/src/cli/commands/migration_compare.ts +152 -0
- package/src/cli/commands/migration_fresh.ts +123 -0
- package/src/cli/commands/migration_generate.ts +79 -0
- package/src/cli/commands/migration_rollback.ts +53 -0
- package/src/cli/commands/migration_run.ts +44 -0
- package/src/cli/commands/queue_flush.ts +35 -0
- package/src/cli/commands/queue_retry.ts +34 -0
- package/src/cli/commands/queue_work.ts +40 -0
- package/src/cli/commands/scheduler_work.ts +45 -0
- package/src/cli/strav.ts +33 -0
- package/src/config/configuration.ts +105 -0
- package/src/config/loaders/base_loader.ts +69 -0
- package/src/config/loaders/env_loader.ts +112 -0
- package/src/config/loaders/typescript_loader.ts +56 -0
- package/src/config/types.ts +8 -0
- package/src/core/application.ts +4 -0
- package/src/core/container.ts +117 -0
- package/src/core/index.ts +3 -0
- package/src/core/inject.ts +39 -0
- package/src/database/database.ts +54 -0
- package/src/database/index.ts +30 -0
- package/src/database/introspector.ts +446 -0
- package/src/database/migration/differ.ts +308 -0
- package/src/database/migration/file_generator.ts +125 -0
- package/src/database/migration/index.ts +18 -0
- package/src/database/migration/runner.ts +133 -0
- package/src/database/migration/sql_generator.ts +378 -0
- package/src/database/migration/tracker.ts +76 -0
- package/src/database/migration/types.ts +189 -0
- package/src/database/query_builder.ts +474 -0
- package/src/encryption/encryption_manager.ts +209 -0
- package/src/encryption/helpers.ts +158 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/types.ts +6 -0
- package/src/events/emitter.ts +101 -0
- package/src/events/index.ts +2 -0
- package/src/exceptions/errors.ts +75 -0
- package/src/exceptions/exception_handler.ts +126 -0
- package/src/exceptions/helpers.ts +25 -0
- package/src/exceptions/http_exception.ts +129 -0
- package/src/exceptions/index.ts +23 -0
- package/src/exceptions/strav_error.ts +11 -0
- package/src/generators/api_generator.ts +972 -0
- package/src/generators/config.ts +87 -0
- package/src/generators/doc_generator.ts +974 -0
- package/src/generators/index.ts +11 -0
- package/src/generators/model_generator.ts +586 -0
- package/src/generators/route_generator.ts +188 -0
- package/src/generators/test_generator.ts +1666 -0
- package/src/helpers/crypto.ts +4 -0
- package/src/helpers/env.ts +50 -0
- package/src/helpers/identity.ts +12 -0
- package/src/helpers/index.ts +4 -0
- package/src/helpers/strings.ts +67 -0
- package/src/http/context.ts +215 -0
- package/src/http/cookie.ts +59 -0
- package/src/http/cors.ts +163 -0
- package/src/http/index.ts +16 -0
- package/src/http/middleware.ts +39 -0
- package/src/http/rate_limit.ts +173 -0
- package/src/http/router.ts +556 -0
- package/src/http/server.ts +79 -0
- package/src/i18n/defaults/en/validation.json +20 -0
- package/src/i18n/helpers.ts +72 -0
- package/src/i18n/i18n_manager.ts +155 -0
- package/src/i18n/index.ts +4 -0
- package/src/i18n/middleware.ts +90 -0
- package/src/i18n/translator.ts +96 -0
- package/src/i18n/types.ts +17 -0
- package/src/logger/index.ts +6 -0
- package/src/logger/logger.ts +100 -0
- package/src/logger/request_logger.ts +19 -0
- package/src/logger/sinks/console_sink.ts +24 -0
- package/src/logger/sinks/file_sink.ts +24 -0
- package/src/logger/sinks/sink.ts +36 -0
- package/src/mail/css_inliner.ts +79 -0
- package/src/mail/helpers.ts +212 -0
- package/src/mail/index.ts +19 -0
- package/src/mail/mail_manager.ts +92 -0
- package/src/mail/transports/log_transport.ts +69 -0
- package/src/mail/transports/resend_transport.ts +59 -0
- package/src/mail/transports/sendgrid_transport.ts +77 -0
- package/src/mail/transports/smtp_transport.ts +48 -0
- package/src/mail/types.ts +80 -0
- package/src/notification/base_notification.ts +67 -0
- package/src/notification/channels/database_channel.ts +30 -0
- package/src/notification/channels/discord_channel.ts +43 -0
- package/src/notification/channels/email_channel.ts +37 -0
- package/src/notification/channels/webhook_channel.ts +45 -0
- package/src/notification/helpers.ts +214 -0
- package/src/notification/index.ts +20 -0
- package/src/notification/notification_manager.ts +126 -0
- package/src/notification/types.ts +122 -0
- package/src/orm/base_model.ts +351 -0
- package/src/orm/decorators.ts +127 -0
- package/src/orm/index.ts +4 -0
- package/src/policy/authorize.ts +44 -0
- package/src/policy/index.ts +3 -0
- package/src/policy/policy_result.ts +13 -0
- package/src/queue/index.ts +11 -0
- package/src/queue/queue.ts +338 -0
- package/src/queue/worker.ts +197 -0
- package/src/scheduler/cron.ts +140 -0
- package/src/scheduler/index.ts +7 -0
- package/src/scheduler/runner.ts +116 -0
- package/src/scheduler/schedule.ts +183 -0
- package/src/scheduler/scheduler.ts +47 -0
- package/src/schema/database_representation.ts +122 -0
- package/src/schema/define_association.ts +60 -0
- package/src/schema/define_schema.ts +46 -0
- package/src/schema/field_builder.ts +155 -0
- package/src/schema/field_definition.ts +66 -0
- package/src/schema/index.ts +21 -0
- package/src/schema/naming.ts +19 -0
- package/src/schema/postgres.ts +109 -0
- package/src/schema/registry.ts +157 -0
- package/src/schema/representation_builder.ts +479 -0
- package/src/schema/type_builder.ts +107 -0
- package/src/schema/types.ts +35 -0
- package/src/session/index.ts +4 -0
- package/src/session/middleware.ts +46 -0
- package/src/session/session.ts +308 -0
- package/src/session/session_manager.ts +81 -0
- package/src/storage/index.ts +13 -0
- package/src/storage/local_driver.ts +46 -0
- package/src/storage/s3_driver.ts +51 -0
- package/src/storage/storage.ts +43 -0
- package/src/storage/storage_manager.ts +59 -0
- package/src/storage/types.ts +42 -0
- package/src/storage/upload.ts +91 -0
- package/src/validation/index.ts +18 -0
- package/src/validation/rules.ts +170 -0
- package/src/validation/validate.ts +41 -0
- package/src/view/cache.ts +47 -0
- package/src/view/client/islands.ts +50 -0
- package/src/view/compiler.ts +185 -0
- package/src/view/engine.ts +139 -0
- package/src/view/escape.ts +14 -0
- package/src/view/index.ts +13 -0
- package/src/view/islands/island_builder.ts +161 -0
- package/src/view/islands/vue_plugin.ts +140 -0
- package/src/view/middleware/static.ts +35 -0
- package/src/view/tokenizer.ts +172 -0
- 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,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
|
+
}
|