kysely-schema 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/.turbo/turbo-build.log +20 -0
- package/dist/index.d.mts +207 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +573 -0
- package/dist/index.mjs +535 -0
- package/package.json +48 -0
- package/src/differ/index.ts +150 -0
- package/src/differ/operations.ts +33 -0
- package/src/generators/migration.ts +172 -0
- package/src/generators/templates.ts +33 -0
- package/src/generators/types.ts +91 -0
- package/src/index.ts +40 -0
- package/src/schema/dsl.ts +125 -0
- package/src/schema/types.ts +158 -0
- package/src/schema/validators.ts +113 -0
- package/tests/differ.test.ts +161 -0
- package/tests/dsl.test.ts +168 -0
- package/tests/migration.test.ts +140 -0
- package/tests/types.test.ts +176 -0
- package/tests/validators.test.ts +116 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// ─── Referential Actions ───────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type ReferentialAction = 'cascade' | 'set null' | 'restrict' | 'no action';
|
|
4
|
+
|
|
5
|
+
// ─── Column Types ──────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export type ColumnType =
|
|
8
|
+
| 'serial'
|
|
9
|
+
| 'integer'
|
|
10
|
+
| 'bigint'
|
|
11
|
+
| 'decimal'
|
|
12
|
+
| 'text'
|
|
13
|
+
| 'varchar'
|
|
14
|
+
| 'timestamp'
|
|
15
|
+
| 'date'
|
|
16
|
+
| 'time'
|
|
17
|
+
| 'boolean'
|
|
18
|
+
| 'json'
|
|
19
|
+
| 'jsonb'
|
|
20
|
+
| 'binary'
|
|
21
|
+
| 'uuid';
|
|
22
|
+
|
|
23
|
+
// ─── Column Definition ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface ForeignKeyReference {
|
|
26
|
+
table: string;
|
|
27
|
+
column: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ColumnDefinition {
|
|
31
|
+
type: ColumnType;
|
|
32
|
+
primaryKey?: boolean;
|
|
33
|
+
notNull?: boolean;
|
|
34
|
+
nullable?: boolean;
|
|
35
|
+
unique?: boolean;
|
|
36
|
+
default?: string | number | boolean;
|
|
37
|
+
references?: ForeignKeyReference;
|
|
38
|
+
onDelete?: ReferentialAction;
|
|
39
|
+
onUpdate?: ReferentialAction;
|
|
40
|
+
index?: boolean;
|
|
41
|
+
check?: string;
|
|
42
|
+
length?: number;
|
|
43
|
+
precision?: number;
|
|
44
|
+
scale?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Table Definition ─────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export interface IndexDefinition {
|
|
50
|
+
name?: string;
|
|
51
|
+
columns: string[];
|
|
52
|
+
unique?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TableDefinition {
|
|
56
|
+
columns: Record<string, ColumnDefinition>;
|
|
57
|
+
indexes: IndexDefinition[];
|
|
58
|
+
checks?: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Schema Definition ────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export interface SchemaDefinition {
|
|
64
|
+
tables: Record<string, TableDefinition>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Migration File ───────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export interface MigrationFile {
|
|
70
|
+
filename: string;
|
|
71
|
+
content: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Diff Operations ──────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export type DiffOperationType =
|
|
77
|
+
| 'addTable'
|
|
78
|
+
| 'dropTable'
|
|
79
|
+
| 'addColumn'
|
|
80
|
+
| 'dropColumn'
|
|
81
|
+
| 'alterColumn'
|
|
82
|
+
| 'addIndex'
|
|
83
|
+
| 'dropIndex';
|
|
84
|
+
|
|
85
|
+
export interface BaseDiffOperation {
|
|
86
|
+
type: DiffOperationType;
|
|
87
|
+
description: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface AddTableOperation extends BaseDiffOperation {
|
|
91
|
+
type: 'addTable';
|
|
92
|
+
tableName: string;
|
|
93
|
+
table: TableDefinition;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface DropTableOperation extends BaseDiffOperation {
|
|
97
|
+
type: 'dropTable';
|
|
98
|
+
tableName: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AddColumnOperation extends BaseDiffOperation {
|
|
102
|
+
type: 'addColumn';
|
|
103
|
+
tableName: string;
|
|
104
|
+
columnName: string;
|
|
105
|
+
column: ColumnDefinition;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface DropColumnOperation extends BaseDiffOperation {
|
|
109
|
+
type: 'dropColumn';
|
|
110
|
+
tableName: string;
|
|
111
|
+
columnName: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface AlterColumnOperation extends BaseDiffOperation {
|
|
115
|
+
type: 'alterColumn';
|
|
116
|
+
tableName: string;
|
|
117
|
+
columnName: string;
|
|
118
|
+
oldColumn: ColumnDefinition;
|
|
119
|
+
newColumn: ColumnDefinition;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface AddIndexOperation extends BaseDiffOperation {
|
|
123
|
+
type: 'addIndex';
|
|
124
|
+
tableName: string;
|
|
125
|
+
index: IndexDefinition;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface DropIndexOperation extends BaseDiffOperation {
|
|
129
|
+
type: 'dropIndex';
|
|
130
|
+
tableName: string;
|
|
131
|
+
indexName: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type DiffOperation =
|
|
135
|
+
| AddTableOperation
|
|
136
|
+
| DropTableOperation
|
|
137
|
+
| AddColumnOperation
|
|
138
|
+
| DropColumnOperation
|
|
139
|
+
| AlterColumnOperation
|
|
140
|
+
| AddIndexOperation
|
|
141
|
+
| DropIndexOperation;
|
|
142
|
+
|
|
143
|
+
// ─── Schema Snapshot (used by differ) ─────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export interface SchemaSnapshot {
|
|
146
|
+
version: string;
|
|
147
|
+
timestamp: string;
|
|
148
|
+
schema: SchemaDefinition;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export interface KyselySchemaConfig {
|
|
154
|
+
schemaPath: string;
|
|
155
|
+
migrationsDir: string;
|
|
156
|
+
generatedDir: string;
|
|
157
|
+
snapshotDir: string;
|
|
158
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { SchemaDefinition } from './types.js';
|
|
2
|
+
|
|
3
|
+
export interface ValidationError {
|
|
4
|
+
table: string;
|
|
5
|
+
column?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate a schema definition and return a list of problems.
|
|
11
|
+
* Returns an empty array if the schema is valid.
|
|
12
|
+
*/
|
|
13
|
+
export function validateSchema(schema: SchemaDefinition): ValidationError[] {
|
|
14
|
+
const errors: ValidationError[] = [];
|
|
15
|
+
const tableNames = Object.keys(schema.tables);
|
|
16
|
+
|
|
17
|
+
for (const [tableName, tableDef] of Object.entries(schema.tables)) {
|
|
18
|
+
const columnNames = Object.keys(tableDef.columns);
|
|
19
|
+
|
|
20
|
+
// ── check: table must have at least one column ──
|
|
21
|
+
if (columnNames.length === 0) {
|
|
22
|
+
errors.push({
|
|
23
|
+
table: tableName,
|
|
24
|
+
message: 'Table has no columns defined.',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── check: at least one primary key ──
|
|
29
|
+
const pks = columnNames.filter((c) => tableDef.columns[c].primaryKey);
|
|
30
|
+
if (pks.length === 0) {
|
|
31
|
+
errors.push({
|
|
32
|
+
table: tableName,
|
|
33
|
+
message: 'Table has no primary key defined.',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [colName, colDef] of Object.entries(tableDef.columns)) {
|
|
38
|
+
// ── check: notNull + nullable conflict ──
|
|
39
|
+
if (colDef.notNull && colDef.nullable) {
|
|
40
|
+
errors.push({
|
|
41
|
+
table: tableName,
|
|
42
|
+
column: colName,
|
|
43
|
+
message: 'Column cannot be both notNull and nullable.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── check: foreign key target exists ──
|
|
48
|
+
if (colDef.references) {
|
|
49
|
+
if (!tableNames.includes(colDef.references.table)) {
|
|
50
|
+
errors.push({
|
|
51
|
+
table: tableName,
|
|
52
|
+
column: colName,
|
|
53
|
+
message: `Foreign key references unknown table '${colDef.references.table}'.`,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
const targetTable = schema.tables[colDef.references.table];
|
|
57
|
+
if (
|
|
58
|
+
targetTable &&
|
|
59
|
+
!Object.keys(targetTable.columns).includes(colDef.references.column)
|
|
60
|
+
) {
|
|
61
|
+
errors.push({
|
|
62
|
+
table: tableName,
|
|
63
|
+
column: colName,
|
|
64
|
+
message: `Foreign key references unknown column '${colDef.references.column}' in table '${colDef.references.table}'.`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── check: onDelete / onUpdate without references ──
|
|
71
|
+
if ((colDef.onDelete || colDef.onUpdate) && !colDef.references) {
|
|
72
|
+
errors.push({
|
|
73
|
+
table: tableName,
|
|
74
|
+
column: colName,
|
|
75
|
+
message: 'onDelete/onUpdate specified without a foreign key reference.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── check: varchar length ──
|
|
80
|
+
if (colDef.type === 'varchar' && colDef.length !== undefined && colDef.length <= 0) {
|
|
81
|
+
errors.push({
|
|
82
|
+
table: tableName,
|
|
83
|
+
column: colName,
|
|
84
|
+
message: 'varchar length must be a positive number.',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── check: decimal precision/scale ──
|
|
89
|
+
if (colDef.type === 'decimal') {
|
|
90
|
+
if (colDef.precision !== undefined && colDef.precision <= 0) {
|
|
91
|
+
errors.push({
|
|
92
|
+
table: tableName,
|
|
93
|
+
column: colName,
|
|
94
|
+
message: 'decimal precision must be a positive number.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
colDef.scale !== undefined &&
|
|
99
|
+
colDef.precision !== undefined &&
|
|
100
|
+
colDef.scale > colDef.precision
|
|
101
|
+
) {
|
|
102
|
+
errors.push({
|
|
103
|
+
table: tableName,
|
|
104
|
+
column: colName,
|
|
105
|
+
message: 'decimal scale cannot exceed precision.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return errors;
|
|
113
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SchemaDiffer } from '../src/differ/index.js';
|
|
3
|
+
import { defineSchema, table, column } from '../src/schema/dsl.js';
|
|
4
|
+
|
|
5
|
+
describe('SchemaDiffer', () => {
|
|
6
|
+
const differ = new SchemaDiffer();
|
|
7
|
+
|
|
8
|
+
it('detects no changes for identical schemas', () => {
|
|
9
|
+
const schema = defineSchema({
|
|
10
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
11
|
+
});
|
|
12
|
+
const ops = differ.diff(schema, schema);
|
|
13
|
+
expect(ops).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('detects added tables', () => {
|
|
17
|
+
const prev = defineSchema({
|
|
18
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
19
|
+
});
|
|
20
|
+
const curr = defineSchema({
|
|
21
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
22
|
+
post: table({ id: column.serial().primaryKey() }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const ops = differ.diff(prev, curr);
|
|
26
|
+
expect(ops).toHaveLength(1);
|
|
27
|
+
expect(ops[0].type).toBe('addTable');
|
|
28
|
+
if (ops[0].type === 'addTable') {
|
|
29
|
+
expect(ops[0].tableName).toBe('post');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('detects dropped tables', () => {
|
|
34
|
+
const prev = defineSchema({
|
|
35
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
36
|
+
post: table({ id: column.serial().primaryKey() }),
|
|
37
|
+
});
|
|
38
|
+
const curr = defineSchema({
|
|
39
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ops = differ.diff(prev, curr);
|
|
43
|
+
expect(ops).toHaveLength(1);
|
|
44
|
+
expect(ops[0].type).toBe('dropTable');
|
|
45
|
+
if (ops[0].type === 'dropTable') {
|
|
46
|
+
expect(ops[0].tableName).toBe('post');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('detects added columns', () => {
|
|
51
|
+
const prev = defineSchema({
|
|
52
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
53
|
+
});
|
|
54
|
+
const curr = defineSchema({
|
|
55
|
+
user: table({
|
|
56
|
+
id: column.serial().primaryKey(),
|
|
57
|
+
email: column.text().notNull(),
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const ops = differ.diff(prev, curr);
|
|
62
|
+
expect(ops).toHaveLength(1);
|
|
63
|
+
expect(ops[0].type).toBe('addColumn');
|
|
64
|
+
if (ops[0].type === 'addColumn') {
|
|
65
|
+
expect(ops[0].tableName).toBe('user');
|
|
66
|
+
expect(ops[0].columnName).toBe('email');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('detects dropped columns', () => {
|
|
71
|
+
const prev = defineSchema({
|
|
72
|
+
user: table({
|
|
73
|
+
id: column.serial().primaryKey(),
|
|
74
|
+
email: column.text().notNull(),
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const curr = defineSchema({
|
|
78
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const ops = differ.diff(prev, curr);
|
|
82
|
+
expect(ops).toHaveLength(1);
|
|
83
|
+
expect(ops[0].type).toBe('dropColumn');
|
|
84
|
+
if (ops[0].type === 'dropColumn') {
|
|
85
|
+
expect(ops[0].tableName).toBe('user');
|
|
86
|
+
expect(ops[0].columnName).toBe('email');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('detects altered columns', () => {
|
|
91
|
+
const prev = defineSchema({
|
|
92
|
+
user: table({
|
|
93
|
+
id: column.serial().primaryKey(),
|
|
94
|
+
name: column.text().notNull(),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
const curr = defineSchema({
|
|
98
|
+
user: table({
|
|
99
|
+
id: column.serial().primaryKey(),
|
|
100
|
+
name: column.varchar(200).notNull(),
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const ops = differ.diff(prev, curr);
|
|
105
|
+
expect(ops).toHaveLength(1);
|
|
106
|
+
expect(ops[0].type).toBe('alterColumn');
|
|
107
|
+
if (ops[0].type === 'alterColumn') {
|
|
108
|
+
expect(ops[0].tableName).toBe('user');
|
|
109
|
+
expect(ops[0].columnName).toBe('name');
|
|
110
|
+
expect(ops[0].oldColumn.type).toBe('text');
|
|
111
|
+
expect(ops[0].newColumn.type).toBe('varchar');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('detects multiple changes at once', () => {
|
|
116
|
+
const prev = defineSchema({
|
|
117
|
+
user: table({
|
|
118
|
+
id: column.serial().primaryKey(),
|
|
119
|
+
email: column.text().notNull(),
|
|
120
|
+
}),
|
|
121
|
+
post: table({ id: column.serial().primaryKey() }),
|
|
122
|
+
});
|
|
123
|
+
const curr = defineSchema({
|
|
124
|
+
user: table({
|
|
125
|
+
id: column.serial().primaryKey(),
|
|
126
|
+
email: column.varchar(255).notNull(),
|
|
127
|
+
name: column.text().nullable(),
|
|
128
|
+
}),
|
|
129
|
+
comment: table({ id: column.serial().primaryKey() }),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const ops = differ.diff(prev, curr);
|
|
133
|
+
const types = ops.map((o) => o.type);
|
|
134
|
+
|
|
135
|
+
expect(types).toContain('addTable'); // comment
|
|
136
|
+
expect(types).toContain('dropTable'); // post
|
|
137
|
+
expect(types).toContain('addColumn'); // user.name
|
|
138
|
+
expect(types).toContain('alterColumn'); // user.email
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('generateAlterMigration()', () => {
|
|
142
|
+
it('generates ALTER TABLE for add column', () => {
|
|
143
|
+
const prev = defineSchema({
|
|
144
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
145
|
+
});
|
|
146
|
+
const curr = defineSchema({
|
|
147
|
+
user: table({
|
|
148
|
+
id: column.serial().primaryKey(),
|
|
149
|
+
name: column.text().nullable(),
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const ops = differ.diff(prev, curr);
|
|
154
|
+
const migration = differ.generateAlterMigration(ops);
|
|
155
|
+
|
|
156
|
+
expect(migration.up).toContain("alterTable('user')");
|
|
157
|
+
expect(migration.up).toContain("addColumn('name', 'text')");
|
|
158
|
+
expect(migration.down).toContain("dropColumn('name')");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { column, table, defineSchema, ColumnBuilder } from '../src/schema/dsl.js';
|
|
3
|
+
|
|
4
|
+
describe('ColumnBuilder', () => {
|
|
5
|
+
it('creates a serial column', () => {
|
|
6
|
+
const col = column.serial().build();
|
|
7
|
+
expect(col.type).toBe('serial');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('chains primaryKey()', () => {
|
|
11
|
+
const col = column.serial().primaryKey().build();
|
|
12
|
+
expect(col.primaryKey).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('chains notNull()', () => {
|
|
16
|
+
const col = column.text().notNull().build();
|
|
17
|
+
expect(col.notNull).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('chains nullable()', () => {
|
|
21
|
+
const col = column.text().nullable().build();
|
|
22
|
+
expect(col.nullable).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('chains unique()', () => {
|
|
26
|
+
const col = column.text().unique().build();
|
|
27
|
+
expect(col.unique).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('chains default() with string', () => {
|
|
31
|
+
const col = column.text().default('hello').build();
|
|
32
|
+
expect(col.default).toBe('hello');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('chains default() with boolean', () => {
|
|
36
|
+
const col = column.boolean().default(false).build();
|
|
37
|
+
expect(col.default).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('chains default() with number', () => {
|
|
41
|
+
const col = column.integer().default(42).build();
|
|
42
|
+
expect(col.default).toBe(42);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('chains references()', () => {
|
|
46
|
+
const col = column.integer().references('user', 'id').build();
|
|
47
|
+
expect(col.references).toEqual({ table: 'user', column: 'id' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('chains onDelete() and onUpdate()', () => {
|
|
51
|
+
const col = column
|
|
52
|
+
.integer()
|
|
53
|
+
.references('user', 'id')
|
|
54
|
+
.onDelete('cascade')
|
|
55
|
+
.onUpdate('set null')
|
|
56
|
+
.build();
|
|
57
|
+
expect(col.onDelete).toBe('cascade');
|
|
58
|
+
expect(col.onUpdate).toBe('set null');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('chains index()', () => {
|
|
62
|
+
const col = column.integer().index().build();
|
|
63
|
+
expect(col.index).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('chains check()', () => {
|
|
67
|
+
const col = column.integer().check('value > 0').build();
|
|
68
|
+
expect(col.check).toBe('value > 0');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('column factory', () => {
|
|
73
|
+
it.each([
|
|
74
|
+
['serial', column.serial],
|
|
75
|
+
['integer', column.integer],
|
|
76
|
+
['bigint', column.bigint],
|
|
77
|
+
['text', column.text],
|
|
78
|
+
['boolean', column.boolean],
|
|
79
|
+
['timestamp', column.timestamp],
|
|
80
|
+
['date', column.date],
|
|
81
|
+
['time', column.time],
|
|
82
|
+
['json', column.json],
|
|
83
|
+
['jsonb', column.jsonb],
|
|
84
|
+
['binary', column.binary],
|
|
85
|
+
['uuid', column.uuid],
|
|
86
|
+
] as const)('creates %s column', (type, factory) => {
|
|
87
|
+
const col = factory().build();
|
|
88
|
+
expect(col.type).toBe(type);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('creates varchar with length', () => {
|
|
92
|
+
const col = column.varchar(255).build();
|
|
93
|
+
expect(col.type).toBe('varchar');
|
|
94
|
+
expect(col.length).toBe(255);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('creates decimal with precision and scale', () => {
|
|
98
|
+
const col = column.decimal(10, 2).build();
|
|
99
|
+
expect(col.type).toBe('decimal');
|
|
100
|
+
expect(col.precision).toBe(10);
|
|
101
|
+
expect(col.scale).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('table()', () => {
|
|
106
|
+
it('converts ColumnBuilders to ColumnDefinitions', () => {
|
|
107
|
+
const t = table({
|
|
108
|
+
id: column.serial().primaryKey(),
|
|
109
|
+
name: column.text().notNull(),
|
|
110
|
+
});
|
|
111
|
+
expect(t.columns.id.type).toBe('serial');
|
|
112
|
+
expect(t.columns.id.primaryKey).toBe(true);
|
|
113
|
+
expect(t.columns.name.type).toBe('text');
|
|
114
|
+
expect(t.columns.name.notNull).toBe(true);
|
|
115
|
+
expect(t.indexes).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('defineSchema()', () => {
|
|
120
|
+
it('wraps tables into a SchemaDefinition', () => {
|
|
121
|
+
const schema = defineSchema({
|
|
122
|
+
user: table({
|
|
123
|
+
id: column.serial().primaryKey(),
|
|
124
|
+
email: column.text().notNull().unique(),
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
expect(schema.tables).toBeDefined();
|
|
128
|
+
expect(schema.tables.user).toBeDefined();
|
|
129
|
+
expect(schema.tables.user.columns.id.primaryKey).toBe(true);
|
|
130
|
+
expect(schema.tables.user.columns.email.unique).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('supports multiple tables', () => {
|
|
134
|
+
const schema = defineSchema({
|
|
135
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
136
|
+
post: table({ id: column.serial().primaryKey() }),
|
|
137
|
+
});
|
|
138
|
+
expect(Object.keys(schema.tables)).toEqual(['user', 'post']);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('complex schema', () => {
|
|
143
|
+
it('builds a blog schema with relations', () => {
|
|
144
|
+
const schema = defineSchema({
|
|
145
|
+
user: table({
|
|
146
|
+
id: column.serial().primaryKey(),
|
|
147
|
+
email: column.text().notNull().unique(),
|
|
148
|
+
name: column.text().nullable(),
|
|
149
|
+
createdAt: column.timestamp().default('now()').notNull(),
|
|
150
|
+
}),
|
|
151
|
+
post: table({
|
|
152
|
+
id: column.serial().primaryKey(),
|
|
153
|
+
title: column.text().notNull(),
|
|
154
|
+
authorId: column
|
|
155
|
+
.integer()
|
|
156
|
+
.notNull()
|
|
157
|
+
.references('user', 'id')
|
|
158
|
+
.onDelete('cascade'),
|
|
159
|
+
createdAt: column.timestamp().default('now()').notNull(),
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const postAuthorId = schema.tables.post.columns.authorId;
|
|
164
|
+
expect(postAuthorId.references).toEqual({ table: 'user', column: 'id' });
|
|
165
|
+
expect(postAuthorId.onDelete).toBe('cascade');
|
|
166
|
+
expect(postAuthorId.notNull).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|