@fragno-dev/db 0.0.1
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/.turbo/turbo-build.log +17 -0
- package/.turbo/turbo-types$colon$check.log +1 -0
- package/README.md +13 -0
- package/dist/cuid.js +3 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +6 -0
- package/dist/mod.js.map +1 -0
- package/dist/schema/create.d.ts +282 -0
- package/dist/schema/create.d.ts.map +1 -0
- package/dist/schema/create.js +437 -0
- package/dist/schema/create.js.map +1 -0
- package/package.json +34 -0
- package/src/cuid.ts +3 -0
- package/src/migration-engine/auto-from-schema.test.ts +238 -0
- package/src/migration-engine/auto-from-schema.ts +106 -0
- package/src/migration-engine/create.test.ts +349 -0
- package/src/migration-engine/create.ts +199 -0
- package/src/migration-engine/shared.ts +102 -0
- package/src/mod.ts +1 -0
- package/src/schema/create.test.ts +287 -0
- package/src/schema/create.ts +809 -0
- package/src/shared/config.ts +18 -0
- package/src/shared/prisma.ts +45 -0
- package/src/shared/providers.ts +10 -0
- package/src/util/deep-equal.ts +29 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +7 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import { createId } from "../cuid";
|
|
2
|
+
|
|
3
|
+
export type AnySchema = Schema<Record<string, AnyTable>>;
|
|
4
|
+
|
|
5
|
+
export type AnyRelation = Relation;
|
|
6
|
+
|
|
7
|
+
export type AnyTable = Table;
|
|
8
|
+
|
|
9
|
+
export type AnyColumn =
|
|
10
|
+
| Column<keyof TypeMap, unknown, unknown>
|
|
11
|
+
| IdColumn<IdColumnType, unknown, unknown>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Operations that can be performed on a table during its definition.
|
|
15
|
+
*/
|
|
16
|
+
export type TableOperation = {
|
|
17
|
+
type: "add-index";
|
|
18
|
+
name: string;
|
|
19
|
+
columns: string[];
|
|
20
|
+
unique: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Operations that can be performed on a schema during its definition.
|
|
25
|
+
* These are tracked so we can generate migrations for specific version ranges.
|
|
26
|
+
*/
|
|
27
|
+
export type SchemaOperation =
|
|
28
|
+
| {
|
|
29
|
+
type: "add-table";
|
|
30
|
+
tableName: string;
|
|
31
|
+
table: AnyTable;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: "add-reference";
|
|
35
|
+
tableName: string;
|
|
36
|
+
referenceName: string;
|
|
37
|
+
config: {
|
|
38
|
+
columns: string[];
|
|
39
|
+
targetTable: string;
|
|
40
|
+
targetColumns: string[];
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: "add-index";
|
|
45
|
+
tableName: string;
|
|
46
|
+
name: string;
|
|
47
|
+
columns: string[];
|
|
48
|
+
unique: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface ForeignKey {
|
|
52
|
+
name: string;
|
|
53
|
+
table: AnyTable;
|
|
54
|
+
columns: AnyColumn[];
|
|
55
|
+
|
|
56
|
+
referencedTable: AnyTable;
|
|
57
|
+
referencedColumns: AnyColumn[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class RelationInit<
|
|
61
|
+
TRelationType extends RelationType,
|
|
62
|
+
TTables extends Record<string, AnyTable>,
|
|
63
|
+
TTableName extends keyof TTables,
|
|
64
|
+
> {
|
|
65
|
+
type: TRelationType;
|
|
66
|
+
referencedTable: TTables[TTableName];
|
|
67
|
+
referencer: AnyTable;
|
|
68
|
+
on: [string, string][] = [];
|
|
69
|
+
|
|
70
|
+
constructor(type: TRelationType, referencedTable: TTables[TTableName], referencer: AnyTable) {
|
|
71
|
+
this.type = type;
|
|
72
|
+
this.referencedTable = referencedTable;
|
|
73
|
+
this.referencer = referencer;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface Index {
|
|
78
|
+
name: string;
|
|
79
|
+
columns: AnyColumn[];
|
|
80
|
+
unique: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Helper function to add an index to a table's index array
|
|
85
|
+
*/
|
|
86
|
+
function addIndexToTable(
|
|
87
|
+
indexes: Index[],
|
|
88
|
+
name: string,
|
|
89
|
+
columns: AnyColumn[],
|
|
90
|
+
unique: boolean,
|
|
91
|
+
): void {
|
|
92
|
+
indexes.push({
|
|
93
|
+
name,
|
|
94
|
+
columns,
|
|
95
|
+
unique,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class ExplicitRelationInit<
|
|
100
|
+
TRelationType extends RelationType,
|
|
101
|
+
TTables extends Record<string, AnyTable>,
|
|
102
|
+
TTableName extends keyof TTables,
|
|
103
|
+
> extends RelationInit<TRelationType, TTables, TTableName> {
|
|
104
|
+
private foreignKeyName?: string;
|
|
105
|
+
|
|
106
|
+
private initForeignKey(ormName: string): ForeignKey {
|
|
107
|
+
const columns: AnyColumn[] = [];
|
|
108
|
+
const referencedColumns: AnyColumn[] = [];
|
|
109
|
+
|
|
110
|
+
for (const [left, right] of this.on) {
|
|
111
|
+
columns.push(this.referencer.columns[left]);
|
|
112
|
+
referencedColumns.push(this.referencedTable.columns[right]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
columns,
|
|
117
|
+
referencedColumns,
|
|
118
|
+
referencedTable: this.referencedTable,
|
|
119
|
+
table: this.referencer,
|
|
120
|
+
name:
|
|
121
|
+
this.foreignKeyName ??
|
|
122
|
+
`${this.referencer.ormName}_${this.referencedTable.ormName}_${ormName}_fk`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
init(ormName: string): Relation<TRelationType, TTables[TTableName]> {
|
|
127
|
+
const id = `${this.referencer.ormName}_${this.referencedTable.ormName}`;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
foreignKey: this.initForeignKey(ormName),
|
|
132
|
+
on: this.on,
|
|
133
|
+
name: ormName,
|
|
134
|
+
referencer: this.referencer,
|
|
135
|
+
table: this.referencedTable,
|
|
136
|
+
type: this.type,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Define custom foreign key name.
|
|
142
|
+
*/
|
|
143
|
+
foreignKey(name: string) {
|
|
144
|
+
this.foreignKeyName = name;
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface Relation<
|
|
150
|
+
TRelationType extends RelationType = RelationType,
|
|
151
|
+
TTable extends AnyTable = AnyTable,
|
|
152
|
+
> {
|
|
153
|
+
id: string;
|
|
154
|
+
name: string;
|
|
155
|
+
type: TRelationType;
|
|
156
|
+
|
|
157
|
+
table: TTable;
|
|
158
|
+
referencer: AnyTable;
|
|
159
|
+
|
|
160
|
+
on: [string, string][];
|
|
161
|
+
foreignKey: ForeignKey;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface Table<
|
|
165
|
+
TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
|
|
166
|
+
TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
|
|
167
|
+
> {
|
|
168
|
+
name: string;
|
|
169
|
+
ormName: string;
|
|
170
|
+
|
|
171
|
+
columns: TColumns;
|
|
172
|
+
relations: TRelations;
|
|
173
|
+
foreignKeys: ForeignKey[];
|
|
174
|
+
indexes: Index[];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get column by name
|
|
178
|
+
*/
|
|
179
|
+
getColumnByName: (name: string) => AnyColumn | undefined;
|
|
180
|
+
getIdColumn: () => AnyColumn;
|
|
181
|
+
|
|
182
|
+
clone: () => Table<TColumns, TRelations>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type DefaultFunctionMap = {
|
|
186
|
+
date: "now";
|
|
187
|
+
timestamp: "now";
|
|
188
|
+
string: "auto";
|
|
189
|
+
} & Record<`varchar(${number})`, "auto">;
|
|
190
|
+
|
|
191
|
+
type DefaultFunction<TType extends keyof TypeMap> =
|
|
192
|
+
| (TType extends keyof DefaultFunctionMap ? DefaultFunctionMap[TType] : never)
|
|
193
|
+
| (() => TypeMap[TType]);
|
|
194
|
+
|
|
195
|
+
type IdColumnType = `varchar(${number})`;
|
|
196
|
+
|
|
197
|
+
export type TypeMap = {
|
|
198
|
+
string: string;
|
|
199
|
+
bigint: bigint;
|
|
200
|
+
integer: number;
|
|
201
|
+
decimal: number;
|
|
202
|
+
bool: boolean;
|
|
203
|
+
json: unknown;
|
|
204
|
+
/**
|
|
205
|
+
* this follows the same specs as Prisma `Bytes` for consistency.
|
|
206
|
+
*/
|
|
207
|
+
binary: Uint8Array;
|
|
208
|
+
date: Date;
|
|
209
|
+
timestamp: Date;
|
|
210
|
+
} & Record<`varchar(${number})`, string>;
|
|
211
|
+
|
|
212
|
+
export class Column<TType extends keyof TypeMap, TIn = unknown, TOut = unknown> {
|
|
213
|
+
type: TType;
|
|
214
|
+
name: string = "";
|
|
215
|
+
ormName: string = "";
|
|
216
|
+
isNullable: boolean = false;
|
|
217
|
+
isUnique: boolean = false;
|
|
218
|
+
isReference: boolean = false;
|
|
219
|
+
default?:
|
|
220
|
+
| { value: TypeMap[TType] }
|
|
221
|
+
| {
|
|
222
|
+
runtime: DefaultFunction<TType>;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
table: AnyTable = undefined as unknown as AnyTable;
|
|
226
|
+
|
|
227
|
+
constructor(type: TType) {
|
|
228
|
+
this.type = type;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
nullable<TNullable extends boolean = true>(nullable?: TNullable) {
|
|
232
|
+
this.isNullable = nullable ?? true;
|
|
233
|
+
|
|
234
|
+
return this as Column<
|
|
235
|
+
TType,
|
|
236
|
+
TNullable extends true ? TIn | null : Exclude<TIn, null>,
|
|
237
|
+
TNullable extends true ? TOut | null : Exclude<TOut, null>
|
|
238
|
+
>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate default value on runtime
|
|
243
|
+
*/
|
|
244
|
+
defaultTo$(fn: DefaultFunction<TType>): Column<TType, TIn | null, TOut> {
|
|
245
|
+
this.default = { runtime: fn };
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set a database-level default value
|
|
251
|
+
*
|
|
252
|
+
* For schemaless database, it's still generated on runtime
|
|
253
|
+
*/
|
|
254
|
+
defaultTo(value: TypeMap[TType]): Column<TType, TIn | null, TOut> {
|
|
255
|
+
this.default = { value };
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
clone() {
|
|
260
|
+
const clone = new Column(this.type);
|
|
261
|
+
clone.name = this.name;
|
|
262
|
+
clone.ormName = this.ormName;
|
|
263
|
+
clone.isNullable = this.isNullable;
|
|
264
|
+
clone.isUnique = this.isUnique;
|
|
265
|
+
clone.isReference = this.isReference;
|
|
266
|
+
clone.default = this.default;
|
|
267
|
+
clone.table = this.table;
|
|
268
|
+
return clone;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getUniqueConstraintName(): string {
|
|
272
|
+
return `unique_c_${this.table.ormName}_${this.ormName}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Generate default value for the column on runtime.
|
|
277
|
+
*/
|
|
278
|
+
generateDefaultValue(): TypeMap[TType] | undefined {
|
|
279
|
+
if (!this.default) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if ("value" in this.default) {
|
|
284
|
+
return this.default.value;
|
|
285
|
+
}
|
|
286
|
+
if (this.default.runtime === "auto") {
|
|
287
|
+
return createId() as TypeMap[TType];
|
|
288
|
+
}
|
|
289
|
+
if (this.default.runtime === "now") {
|
|
290
|
+
return new Date(Date.now()) as TypeMap[TType];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return this.default.runtime();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
get $in(): TIn {
|
|
297
|
+
throw new Error("Type inference only");
|
|
298
|
+
}
|
|
299
|
+
get $out(): TOut {
|
|
300
|
+
throw new Error("Type inference only");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export class IdColumn<
|
|
305
|
+
TType extends IdColumnType = IdColumnType,
|
|
306
|
+
TIn = unknown,
|
|
307
|
+
TOut = unknown,
|
|
308
|
+
> extends Column<TType, TIn, TOut> {
|
|
309
|
+
id = true;
|
|
310
|
+
|
|
311
|
+
clone() {
|
|
312
|
+
const clone = new IdColumn(this.type);
|
|
313
|
+
clone.name = this.name;
|
|
314
|
+
clone.ormName = this.ormName;
|
|
315
|
+
clone.isNullable = this.isNullable;
|
|
316
|
+
clone.isUnique = this.isUnique;
|
|
317
|
+
clone.isReference = this.isReference;
|
|
318
|
+
clone.default = this.default;
|
|
319
|
+
clone.table = this.table;
|
|
320
|
+
return clone;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
override defaultTo$(fn: DefaultFunction<TType>) {
|
|
324
|
+
return super.defaultTo$(fn) as IdColumn<TType, TIn | null, TOut>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
override defaultTo(value: TypeMap[TType]) {
|
|
328
|
+
return super.defaultTo(value) as IdColumn<TType, TIn | null, TOut>;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function column<TType extends keyof TypeMap>(
|
|
333
|
+
type: TType,
|
|
334
|
+
): Column<TType, TypeMap[TType], TypeMap[TType]> {
|
|
335
|
+
return new Column(type);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create a reference column that points to another table.
|
|
340
|
+
* This is used for foreign key relationships.
|
|
341
|
+
*/
|
|
342
|
+
export function referenceColumn<TType extends keyof TypeMap = "varchar(30)">(
|
|
343
|
+
type?: TType,
|
|
344
|
+
): Column<TType, TypeMap[TType], TypeMap[TType]> {
|
|
345
|
+
const actualType = (type ?? "varchar(30)") as TType;
|
|
346
|
+
const col = new Column<TType, TypeMap[TType], TypeMap[TType]>(actualType);
|
|
347
|
+
col.isReference = true;
|
|
348
|
+
return col as Column<TType, TypeMap[TType], TypeMap[TType]>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function idColumn(): IdColumn<"varchar(30)", string, string> {
|
|
352
|
+
const col = new IdColumn<"varchar(30)", string, string>("varchar(30)");
|
|
353
|
+
col.defaultTo$("auto");
|
|
354
|
+
return col as IdColumn<"varchar(30)", string, string>;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
type RelationType = "one";
|
|
358
|
+
|
|
359
|
+
export class TableBuilder<
|
|
360
|
+
TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
|
|
361
|
+
TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
|
|
362
|
+
> {
|
|
363
|
+
#name: string;
|
|
364
|
+
#columns: TColumns;
|
|
365
|
+
#relations: TRelations;
|
|
366
|
+
#foreignKeys: ForeignKey[] = [];
|
|
367
|
+
#indexes: Index[] = [];
|
|
368
|
+
#version: number = 0;
|
|
369
|
+
#ormName: string = "";
|
|
370
|
+
#operations: TableOperation[] = [];
|
|
371
|
+
|
|
372
|
+
constructor(name: string) {
|
|
373
|
+
this.#name = name;
|
|
374
|
+
this.#columns = {} as TColumns;
|
|
375
|
+
this.#relations = {} as TRelations;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Add a column to the table. Increments the version counter.
|
|
380
|
+
*/
|
|
381
|
+
addColumn<TColumnName extends string, TColumn extends AnyColumn>(
|
|
382
|
+
ormName: TColumnName,
|
|
383
|
+
col: TColumn,
|
|
384
|
+
): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations>;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Add a column to the table with simplified syntax. Increments the version counter.
|
|
388
|
+
*/
|
|
389
|
+
addColumn<TColumnName extends string, TType extends keyof TypeMap>(
|
|
390
|
+
ormName: TColumnName,
|
|
391
|
+
type: TType,
|
|
392
|
+
): TableBuilder<
|
|
393
|
+
TColumns & Record<TColumnName, Column<TType, TypeMap[TType], TypeMap[TType]>>,
|
|
394
|
+
TRelations
|
|
395
|
+
>;
|
|
396
|
+
|
|
397
|
+
addColumn<TColumnName extends string, TColumn extends AnyColumn, TType extends keyof TypeMap>(
|
|
398
|
+
ormName: TColumnName,
|
|
399
|
+
colOrType: TColumn | TType,
|
|
400
|
+
): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations> {
|
|
401
|
+
this.#version++;
|
|
402
|
+
|
|
403
|
+
// Create the column if a type string was provided
|
|
404
|
+
const col = typeof colOrType === "string" ? column(colOrType) : colOrType;
|
|
405
|
+
|
|
406
|
+
// Create a new instance to ensure immutability semantics
|
|
407
|
+
const builder = new TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations>(
|
|
408
|
+
this.#name,
|
|
409
|
+
);
|
|
410
|
+
builder.#columns = { ...this.#columns, [ormName]: col } as TColumns &
|
|
411
|
+
Record<TColumnName, TColumn>;
|
|
412
|
+
builder.#relations = this.#relations;
|
|
413
|
+
builder.#foreignKeys = this.#foreignKeys;
|
|
414
|
+
builder.#indexes = this.#indexes;
|
|
415
|
+
builder.#version = this.#version;
|
|
416
|
+
builder.#ormName = this.#ormName;
|
|
417
|
+
builder.#operations = this.#operations;
|
|
418
|
+
|
|
419
|
+
// Set column metadata
|
|
420
|
+
col.ormName = ormName;
|
|
421
|
+
col.name = ormName;
|
|
422
|
+
|
|
423
|
+
return builder;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Create an index on the specified columns. Increments the version counter.
|
|
428
|
+
*/
|
|
429
|
+
createIndex<TColumnName extends string & keyof TColumns>(
|
|
430
|
+
name: string,
|
|
431
|
+
columns: TColumnName[],
|
|
432
|
+
options?: { unique?: boolean },
|
|
433
|
+
): TableBuilder<TColumns, TRelations> {
|
|
434
|
+
this.#version++;
|
|
435
|
+
|
|
436
|
+
const cols = columns.map((name) => {
|
|
437
|
+
const column = this.#columns[name];
|
|
438
|
+
if (!column) {
|
|
439
|
+
throw new Error(`Unknown column name ${name}`);
|
|
440
|
+
}
|
|
441
|
+
return column;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const unique = options?.unique ?? false;
|
|
445
|
+
addIndexToTable(this.#indexes, name, cols, unique);
|
|
446
|
+
|
|
447
|
+
// Record the operation
|
|
448
|
+
this.#operations.push({
|
|
449
|
+
type: "add-index",
|
|
450
|
+
name,
|
|
451
|
+
columns: columns as string[],
|
|
452
|
+
unique,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return this;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Build the final table. This should be called after all columns are added.
|
|
460
|
+
*/
|
|
461
|
+
build(): Table<TColumns, TRelations> {
|
|
462
|
+
let idCol: AnyColumn | undefined;
|
|
463
|
+
|
|
464
|
+
// Use name as ormName if ormName is not set
|
|
465
|
+
const ormName = this.#ormName || this.#name;
|
|
466
|
+
|
|
467
|
+
const table: Table<TColumns, TRelations> = {
|
|
468
|
+
name: this.#name,
|
|
469
|
+
ormName,
|
|
470
|
+
columns: this.#columns,
|
|
471
|
+
relations: this.#relations,
|
|
472
|
+
foreignKeys: this.#foreignKeys,
|
|
473
|
+
indexes: this.#indexes,
|
|
474
|
+
getColumnByName: (name) => {
|
|
475
|
+
return Object.values(this.#columns).find((c) => c.name === name);
|
|
476
|
+
},
|
|
477
|
+
getIdColumn: () => {
|
|
478
|
+
return idCol!;
|
|
479
|
+
},
|
|
480
|
+
clone: () => {
|
|
481
|
+
const cloneColumns: Record<string, AnyColumn> = {};
|
|
482
|
+
|
|
483
|
+
for (const [k, v] of Object.entries(this.#columns)) {
|
|
484
|
+
cloneColumns[k] = v.clone();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const builder = new TableBuilder<TColumns, TRelations>(this.#name);
|
|
488
|
+
builder.#columns = cloneColumns as TColumns;
|
|
489
|
+
builder.#relations = this.#relations;
|
|
490
|
+
builder.#foreignKeys = [...this.#foreignKeys];
|
|
491
|
+
builder.#indexes = [...this.#indexes];
|
|
492
|
+
builder.#version = this.#version;
|
|
493
|
+
builder.#ormName = this.#ormName;
|
|
494
|
+
builder.#operations = [...this.#operations];
|
|
495
|
+
|
|
496
|
+
const cloned = builder.build();
|
|
497
|
+
|
|
498
|
+
return cloned;
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// Set table reference and find id column
|
|
503
|
+
for (const k in this.#columns) {
|
|
504
|
+
const column = this.#columns[k];
|
|
505
|
+
if (!column) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
column.table = table;
|
|
510
|
+
if (column instanceof IdColumn) {
|
|
511
|
+
idCol = column;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (idCol === undefined) {
|
|
516
|
+
throw new Error(`there's no id column in your table ${this.#name}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return table;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get the current version of the table builder.
|
|
524
|
+
*/
|
|
525
|
+
getVersion(): number {
|
|
526
|
+
return this.#version;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Get the operations performed on this table.
|
|
531
|
+
*/
|
|
532
|
+
getOperations(): TableOperation[] {
|
|
533
|
+
return this.#operations;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Create a new table with callback pattern.
|
|
539
|
+
*/
|
|
540
|
+
export function table<
|
|
541
|
+
TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
|
|
542
|
+
TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
|
|
543
|
+
>(
|
|
544
|
+
name: string,
|
|
545
|
+
callback: (
|
|
546
|
+
builder: TableBuilder<Record<string, AnyColumn>, Record<string, AnyRelation>>,
|
|
547
|
+
) => TableBuilder<TColumns, TRelations>,
|
|
548
|
+
): Table<TColumns, TRelations> {
|
|
549
|
+
const builder = new TableBuilder(name);
|
|
550
|
+
const result = callback(builder);
|
|
551
|
+
return result.build();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export interface Schema<TTables extends Record<string, AnyTable> = Record<string, AnyTable>> {
|
|
555
|
+
/**
|
|
556
|
+
* @description The version of the schema, automatically incremented on each change.
|
|
557
|
+
*/
|
|
558
|
+
version: number;
|
|
559
|
+
tables: TTables;
|
|
560
|
+
/**
|
|
561
|
+
* @description Operations performed on this schema, in order.
|
|
562
|
+
* Used to generate migrations for specific version ranges.
|
|
563
|
+
*/
|
|
564
|
+
operations: SchemaOperation[];
|
|
565
|
+
|
|
566
|
+
clone: () => Schema<TTables>;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export class SchemaBuilder<TTables extends Record<string, AnyTable> = Record<string, never>> {
|
|
570
|
+
#tables: TTables;
|
|
571
|
+
#version: number = 0;
|
|
572
|
+
#operations: SchemaOperation[] = [];
|
|
573
|
+
|
|
574
|
+
constructor() {
|
|
575
|
+
this.#tables = {} as TTables;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Add a table to the schema. Increments the version counter.
|
|
580
|
+
*/
|
|
581
|
+
addTable<
|
|
582
|
+
TTableName extends string,
|
|
583
|
+
TColumns extends Record<string, AnyColumn>,
|
|
584
|
+
TRelations extends Record<string, AnyRelation>,
|
|
585
|
+
>(
|
|
586
|
+
ormName: TTableName,
|
|
587
|
+
callback: (
|
|
588
|
+
builder: TableBuilder<Record<string, AnyColumn>, Record<string, AnyRelation>>,
|
|
589
|
+
) => TableBuilder<TColumns, TRelations>,
|
|
590
|
+
): SchemaBuilder<TTables & Record<TTableName, Table<TColumns, TRelations>>> {
|
|
591
|
+
this.#version++;
|
|
592
|
+
|
|
593
|
+
const tableBuilder = new TableBuilder(ormName);
|
|
594
|
+
const result = callback(tableBuilder);
|
|
595
|
+
const builtTable = result.build();
|
|
596
|
+
|
|
597
|
+
// Set table metadata
|
|
598
|
+
builtTable.ormName = ormName;
|
|
599
|
+
|
|
600
|
+
const builder = new SchemaBuilder<TTables & Record<TTableName, Table<TColumns, TRelations>>>();
|
|
601
|
+
builder.#tables = { ...this.#tables, [ormName]: builtTable } as TTables &
|
|
602
|
+
Record<TTableName, Table<TColumns, TRelations>>;
|
|
603
|
+
|
|
604
|
+
// Start with existing operations plus the add-table operation
|
|
605
|
+
const newOperations: SchemaOperation[] = [
|
|
606
|
+
...this.#operations,
|
|
607
|
+
{
|
|
608
|
+
type: "add-table",
|
|
609
|
+
tableName: ormName,
|
|
610
|
+
table: builtTable,
|
|
611
|
+
},
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
// Promote table operations to schema operations and increment version for each
|
|
615
|
+
const tableOps = result.getOperations();
|
|
616
|
+
for (const tableOp of tableOps) {
|
|
617
|
+
if (tableOp.type === "add-index") {
|
|
618
|
+
this.#version++;
|
|
619
|
+
newOperations.push({
|
|
620
|
+
type: "add-index",
|
|
621
|
+
tableName: ormName,
|
|
622
|
+
name: tableOp.name,
|
|
623
|
+
columns: tableOp.columns,
|
|
624
|
+
unique: tableOp.unique,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
builder.#version = this.#version;
|
|
630
|
+
builder.#operations = newOperations;
|
|
631
|
+
|
|
632
|
+
return builder;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Add a foreign key reference from this table to another table.
|
|
637
|
+
*
|
|
638
|
+
* @param tableName - The table that has the foreign key column
|
|
639
|
+
* @param referenceName - A name for this reference (e.g., "author", "category")
|
|
640
|
+
* @param config - Configuration specifying the foreign key mapping
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ```ts
|
|
644
|
+
* // Basic foreign key: post -> user
|
|
645
|
+
* schema(s => s
|
|
646
|
+
* .addTable("users", t => t.addColumn("id", idColumn()))
|
|
647
|
+
* .addTable("posts", t => t
|
|
648
|
+
* .addColumn("id", idColumn())
|
|
649
|
+
* .addColumn("authorId", referenceColumn()))
|
|
650
|
+
* .addReference("posts", "author", {
|
|
651
|
+
* columns: ["authorId"],
|
|
652
|
+
* targetTable: "users",
|
|
653
|
+
* targetColumns: ["id"],
|
|
654
|
+
* })
|
|
655
|
+
* )
|
|
656
|
+
*
|
|
657
|
+
* // Self-referencing foreign key
|
|
658
|
+
* .addReference("users", "inviter", {
|
|
659
|
+
* columns: ["invitedBy"],
|
|
660
|
+
* targetTable: "users",
|
|
661
|
+
* targetColumns: ["id"],
|
|
662
|
+
* })
|
|
663
|
+
*
|
|
664
|
+
* // Multiple foreign keys - call addReference multiple times
|
|
665
|
+
* .addReference("posts", "author", {
|
|
666
|
+
* columns: ["authorId"],
|
|
667
|
+
* targetTable: "users",
|
|
668
|
+
* targetColumns: ["id"],
|
|
669
|
+
* })
|
|
670
|
+
* .addReference("posts", "category", {
|
|
671
|
+
* columns: ["categoryId"],
|
|
672
|
+
* targetTable: "categories",
|
|
673
|
+
* targetColumns: ["id"],
|
|
674
|
+
* })
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
addReference<
|
|
678
|
+
TTableName extends string & keyof TTables,
|
|
679
|
+
TReferencedTableName extends string & keyof TTables,
|
|
680
|
+
>(
|
|
681
|
+
tableName: TTableName,
|
|
682
|
+
referenceName: string,
|
|
683
|
+
config: {
|
|
684
|
+
columns: (keyof TTables[TTableName]["columns"])[];
|
|
685
|
+
targetTable: TReferencedTableName;
|
|
686
|
+
targetColumns: (keyof TTables[TReferencedTableName]["columns"])[];
|
|
687
|
+
},
|
|
688
|
+
): SchemaBuilder<TTables> {
|
|
689
|
+
this.#version++;
|
|
690
|
+
|
|
691
|
+
const table = this.#tables[tableName];
|
|
692
|
+
const referencedTable = this.#tables[config.targetTable];
|
|
693
|
+
|
|
694
|
+
if (!table) {
|
|
695
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
696
|
+
}
|
|
697
|
+
if (!referencedTable) {
|
|
698
|
+
throw new Error(`Referenced table ${config.targetTable} not found in schema`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const { columns, targetColumns } = config;
|
|
702
|
+
|
|
703
|
+
if (columns.length !== targetColumns.length) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`Reference ${referenceName}: columns and targetColumns must have the same length`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// For now, only support single column foreign keys
|
|
710
|
+
if (columns.length !== 1) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`Reference ${referenceName}: currently only single column foreign keys are supported`,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const columnName = columns[0] as string;
|
|
717
|
+
const targetColumnName = targetColumns[0] as string;
|
|
718
|
+
|
|
719
|
+
const column = table.columns[columnName];
|
|
720
|
+
const referencedColumn = referencedTable.columns[targetColumnName];
|
|
721
|
+
|
|
722
|
+
if (!column) {
|
|
723
|
+
throw new Error(`Column ${columnName} not found in table ${tableName}`);
|
|
724
|
+
}
|
|
725
|
+
if (!referencedColumn) {
|
|
726
|
+
throw new Error(`Column ${targetColumnName} not found in table ${config.targetTable}`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Create the relation
|
|
730
|
+
const init = new ExplicitRelationInit("one", referencedTable, table);
|
|
731
|
+
init.on.push([columnName, targetColumnName]);
|
|
732
|
+
const relation = init.init(referenceName);
|
|
733
|
+
|
|
734
|
+
// Add relation and foreign key to the table
|
|
735
|
+
table.relations[referenceName] = relation;
|
|
736
|
+
table.foreignKeys.push(relation.foreignKey);
|
|
737
|
+
|
|
738
|
+
// Record the operation
|
|
739
|
+
this.#operations.push({
|
|
740
|
+
type: "add-reference",
|
|
741
|
+
tableName: tableName as string,
|
|
742
|
+
referenceName,
|
|
743
|
+
config: {
|
|
744
|
+
columns: columns as string[],
|
|
745
|
+
targetTable: config.targetTable as string,
|
|
746
|
+
targetColumns: targetColumns as string[],
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
return this;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Build the final schema. This should be called after all tables are added.
|
|
755
|
+
*/
|
|
756
|
+
build(): Schema<TTables> {
|
|
757
|
+
const operations = this.#operations;
|
|
758
|
+
const version = this.#version;
|
|
759
|
+
const tables = this.#tables;
|
|
760
|
+
|
|
761
|
+
const schema: Schema<TTables> = {
|
|
762
|
+
version,
|
|
763
|
+
tables,
|
|
764
|
+
operations,
|
|
765
|
+
clone: () => {
|
|
766
|
+
const cloneTables: Record<string, AnyTable> = {};
|
|
767
|
+
|
|
768
|
+
for (const [k, v] of Object.entries(tables)) {
|
|
769
|
+
cloneTables[k] = v.clone();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const builder = new SchemaBuilder<TTables>();
|
|
773
|
+
builder.#tables = cloneTables as TTables;
|
|
774
|
+
builder.#version = version;
|
|
775
|
+
builder.#operations = [...operations];
|
|
776
|
+
|
|
777
|
+
return builder.build();
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
return schema;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Get the current version of the schema builder.
|
|
786
|
+
*/
|
|
787
|
+
getVersion(): number {
|
|
788
|
+
return this.#version;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Create a new schema with callback pattern.
|
|
794
|
+
*/
|
|
795
|
+
export function schema<TTables extends Record<string, AnyTable> = Record<string, never>>(
|
|
796
|
+
callback: (builder: SchemaBuilder<Record<string, never>>) => SchemaBuilder<TTables>,
|
|
797
|
+
): Schema<TTables> {
|
|
798
|
+
return callback(new SchemaBuilder()).build();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function compileForeignKey(key: ForeignKey) {
|
|
802
|
+
return {
|
|
803
|
+
name: key.name,
|
|
804
|
+
table: key.table.name,
|
|
805
|
+
referencedTable: key.referencedTable.name,
|
|
806
|
+
referencedColumns: key.referencedColumns.map((col) => col.name),
|
|
807
|
+
columns: key.columns.map((col) => col.name),
|
|
808
|
+
};
|
|
809
|
+
}
|