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,140 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { MigrationGenerator } from '../src/generators/migration.js';
|
|
3
|
+
import { defineSchema, table, column } from '../src/schema/dsl.js';
|
|
4
|
+
|
|
5
|
+
describe('MigrationGenerator', () => {
|
|
6
|
+
const generator = new MigrationGenerator();
|
|
7
|
+
|
|
8
|
+
it('generates a valid migration filename', () => {
|
|
9
|
+
const schema = defineSchema({
|
|
10
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
11
|
+
});
|
|
12
|
+
const migration = generator.generate(schema, 'create_users');
|
|
13
|
+
expect(migration.filename).toMatch(/^\d{8}T\d{6}_create_users\.ts$/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('generates CREATE TABLE with addColumn', () => {
|
|
17
|
+
const schema = defineSchema({
|
|
18
|
+
user: table({
|
|
19
|
+
id: column.serial().primaryKey(),
|
|
20
|
+
email: column.text().notNull().unique(),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const migration = generator.generate(schema, 'initial');
|
|
25
|
+
expect(migration.content).toContain("createTable('user')");
|
|
26
|
+
expect(migration.content).toContain("addColumn('id', 'serial'");
|
|
27
|
+
expect(migration.content).toContain("addColumn('email', 'text'");
|
|
28
|
+
expect(migration.content).toContain('primaryKey()');
|
|
29
|
+
expect(migration.content).toContain('notNull()');
|
|
30
|
+
expect(migration.content).toContain('unique()');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('generates default values', () => {
|
|
34
|
+
const schema = defineSchema({
|
|
35
|
+
user: table({
|
|
36
|
+
id: column.serial().primaryKey(),
|
|
37
|
+
active: column.boolean().default(false).notNull(),
|
|
38
|
+
createdAt: column.timestamp().default('now()').notNull(),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const migration = generator.generate(schema, 'defaults');
|
|
43
|
+
expect(migration.content).toContain('defaultTo(false)');
|
|
44
|
+
expect(migration.content).toContain('defaultTo(sql`now()`)');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('generates foreign key constraints', () => {
|
|
48
|
+
const schema = defineSchema({
|
|
49
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
50
|
+
post: table({
|
|
51
|
+
id: column.serial().primaryKey(),
|
|
52
|
+
authorId: column
|
|
53
|
+
.integer()
|
|
54
|
+
.notNull()
|
|
55
|
+
.references('user', 'id')
|
|
56
|
+
.onDelete('cascade'),
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const migration = generator.generate(schema, 'with_fk');
|
|
61
|
+
expect(migration.content).toContain("references('user.id')");
|
|
62
|
+
expect(migration.content).toContain("onDelete('cascade')");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('generates indexes for foreign keys', () => {
|
|
66
|
+
const schema = defineSchema({
|
|
67
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
68
|
+
post: table({
|
|
69
|
+
id: column.serial().primaryKey(),
|
|
70
|
+
authorId: column.integer().references('user', 'id'),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const migration = generator.generate(schema, 'fk_indexes');
|
|
75
|
+
expect(migration.content).toContain("createIndex('post_authorId_index')");
|
|
76
|
+
expect(migration.content).toContain(".on('post')");
|
|
77
|
+
expect(migration.content).toContain(".column('authorId')");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('generates DROP TABLE in reverse order for down()', () => {
|
|
81
|
+
const schema = defineSchema({
|
|
82
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
83
|
+
post: table({ id: column.serial().primaryKey() }),
|
|
84
|
+
comment: table({ id: column.serial().primaryKey() }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const migration = generator.generate(schema, 'reverse_drop');
|
|
88
|
+
const downSection = migration.content.split('export async function down')[1];
|
|
89
|
+
const userIdx = downSection.indexOf("dropTable('user')");
|
|
90
|
+
const postIdx = downSection.indexOf("dropTable('post')");
|
|
91
|
+
const commentIdx = downSection.indexOf("dropTable('comment')");
|
|
92
|
+
|
|
93
|
+
// comment should be dropped first, then post, then user
|
|
94
|
+
expect(commentIdx).toBeLessThan(postIdx);
|
|
95
|
+
expect(postIdx).toBeLessThan(userIdx);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('generates varchar with length', () => {
|
|
99
|
+
const schema = defineSchema({
|
|
100
|
+
user: table({
|
|
101
|
+
id: column.serial().primaryKey(),
|
|
102
|
+
name: column.varchar(100).notNull(),
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const migration = generator.generate(schema, 'varchar');
|
|
107
|
+
expect(migration.content).toContain("addColumn('name', 'varchar(100)'");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('generates decimal with precision and scale', () => {
|
|
111
|
+
const schema = defineSchema({
|
|
112
|
+
product: table({
|
|
113
|
+
id: column.serial().primaryKey(),
|
|
114
|
+
price: column.decimal(10, 2).notNull(),
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const migration = generator.generate(schema, 'decimal');
|
|
119
|
+
expect(migration.content).toContain("addColumn('price', 'decimal(10, 2)'");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('imports Kysely and sql', () => {
|
|
123
|
+
const schema = defineSchema({
|
|
124
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const migration = generator.generate(schema, 'imports');
|
|
128
|
+
expect(migration.content).toContain("import { Kysely, sql } from 'kysely'");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('generates up and down functions', () => {
|
|
132
|
+
const schema = defineSchema({
|
|
133
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const migration = generator.generate(schema, 'up_down');
|
|
137
|
+
expect(migration.content).toContain('export async function up');
|
|
138
|
+
expect(migration.content).toContain('export async function down');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { TypeGenerator } from '../src/generators/types.js';
|
|
3
|
+
import { defineSchema, table, column } from '../src/schema/dsl.js';
|
|
4
|
+
|
|
5
|
+
describe('TypeGenerator', () => {
|
|
6
|
+
const generator = new TypeGenerator();
|
|
7
|
+
|
|
8
|
+
it('generates a Database interface', () => {
|
|
9
|
+
const schema = defineSchema({
|
|
10
|
+
user: table({
|
|
11
|
+
id: column.serial().primaryKey(),
|
|
12
|
+
email: column.text().notNull(),
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const output = generator.generate(schema);
|
|
17
|
+
expect(output).toContain('export interface Database');
|
|
18
|
+
expect(output).toContain('user: UserTable');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('generates per-table interfaces', () => {
|
|
22
|
+
const schema = defineSchema({
|
|
23
|
+
user: table({
|
|
24
|
+
id: column.serial().primaryKey(),
|
|
25
|
+
name: column.text().notNull(),
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const output = generator.generate(schema);
|
|
30
|
+
expect(output).toContain('export interface UserTable');
|
|
31
|
+
expect(output).toContain('id: Generated<number>');
|
|
32
|
+
expect(output).toContain('name: string');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('maps serial to Generated<number>', () => {
|
|
36
|
+
const schema = defineSchema({
|
|
37
|
+
t: table({ id: column.serial().primaryKey() }),
|
|
38
|
+
});
|
|
39
|
+
const output = generator.generate(schema);
|
|
40
|
+
expect(output).toContain('id: Generated<number>');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('maps integer to number', () => {
|
|
44
|
+
const schema = defineSchema({
|
|
45
|
+
t: table({
|
|
46
|
+
id: column.serial().primaryKey(),
|
|
47
|
+
count: column.integer().notNull(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
const output = generator.generate(schema);
|
|
51
|
+
expect(output).toContain('count: number');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('maps bigint to string', () => {
|
|
55
|
+
const schema = defineSchema({
|
|
56
|
+
t: table({
|
|
57
|
+
id: column.serial().primaryKey(),
|
|
58
|
+
big: column.bigint().notNull(),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
const output = generator.generate(schema);
|
|
62
|
+
expect(output).toContain('big: string');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('maps text and varchar to string', () => {
|
|
66
|
+
const schema = defineSchema({
|
|
67
|
+
t: table({
|
|
68
|
+
id: column.serial().primaryKey(),
|
|
69
|
+
a: column.text().notNull(),
|
|
70
|
+
b: column.varchar(100).notNull(),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
const output = generator.generate(schema);
|
|
74
|
+
expect(output).toContain('a: string');
|
|
75
|
+
expect(output).toContain('b: string');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('maps timestamp and date to Date', () => {
|
|
79
|
+
const schema = defineSchema({
|
|
80
|
+
t: table({
|
|
81
|
+
id: column.serial().primaryKey(),
|
|
82
|
+
ts: column.timestamp().notNull(),
|
|
83
|
+
d: column.date().notNull(),
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
const output = generator.generate(schema);
|
|
87
|
+
expect(output).toContain('ts: Date');
|
|
88
|
+
expect(output).toContain('d: Date');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('maps boolean to boolean', () => {
|
|
92
|
+
const schema = defineSchema({
|
|
93
|
+
t: table({
|
|
94
|
+
id: column.serial().primaryKey(),
|
|
95
|
+
active: column.boolean().notNull(),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
const output = generator.generate(schema);
|
|
99
|
+
expect(output).toContain('active: boolean');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('maps json/jsonb to unknown', () => {
|
|
103
|
+
const schema = defineSchema({
|
|
104
|
+
t: table({
|
|
105
|
+
id: column.serial().primaryKey(),
|
|
106
|
+
data: column.json().notNull(),
|
|
107
|
+
meta: column.jsonb().notNull(),
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
const output = generator.generate(schema);
|
|
111
|
+
expect(output).toContain('data: unknown');
|
|
112
|
+
expect(output).toContain('meta: unknown');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('maps uuid to string', () => {
|
|
116
|
+
const schema = defineSchema({
|
|
117
|
+
t: table({
|
|
118
|
+
id: column.serial().primaryKey(),
|
|
119
|
+
uid: column.uuid().notNull(),
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
const output = generator.generate(schema);
|
|
123
|
+
expect(output).toContain('uid: string');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('marks nullable columns with | null', () => {
|
|
127
|
+
const schema = defineSchema({
|
|
128
|
+
t: table({
|
|
129
|
+
id: column.serial().primaryKey(),
|
|
130
|
+
bio: column.text().nullable(),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
const output = generator.generate(schema);
|
|
134
|
+
expect(output).toContain('bio: string | null');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('wraps columns with default in Generated<>', () => {
|
|
138
|
+
const schema = defineSchema({
|
|
139
|
+
t: table({
|
|
140
|
+
id: column.serial().primaryKey(),
|
|
141
|
+
active: column.boolean().default(true).notNull(),
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
const output = generator.generate(schema);
|
|
145
|
+
expect(output).toContain('active: Generated<boolean>');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('includes auto-generated header comment', () => {
|
|
149
|
+
const schema = defineSchema({
|
|
150
|
+
t: table({ id: column.serial().primaryKey() }),
|
|
151
|
+
});
|
|
152
|
+
const output = generator.generate(schema);
|
|
153
|
+
expect(output).toContain('Auto-generated by kysely-schema');
|
|
154
|
+
expect(output).toContain('DO NOT EDIT');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('imports Generated and ColumnType from kysely', () => {
|
|
158
|
+
const schema = defineSchema({
|
|
159
|
+
t: table({ id: column.serial().primaryKey() }),
|
|
160
|
+
});
|
|
161
|
+
const output = generator.generate(schema);
|
|
162
|
+
expect(output).toContain("from 'kysely'");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('handles PascalCase conversion for multi-word table names', () => {
|
|
166
|
+
const schema = defineSchema({
|
|
167
|
+
user_profile: table({
|
|
168
|
+
id: column.serial().primaryKey(),
|
|
169
|
+
displayName: column.text().notNull(),
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
const output = generator.generate(schema);
|
|
173
|
+
expect(output).toContain('export interface UserProfileTable');
|
|
174
|
+
expect(output).toContain('user_profile: UserProfileTable');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateSchema } from '../src/schema/validators.js';
|
|
3
|
+
import { defineSchema, table, column } from '../src/schema/dsl.js';
|
|
4
|
+
|
|
5
|
+
describe('validateSchema()', () => {
|
|
6
|
+
it('returns no errors for a valid schema', () => {
|
|
7
|
+
const schema = defineSchema({
|
|
8
|
+
user: table({
|
|
9
|
+
id: column.serial().primaryKey(),
|
|
10
|
+
email: column.text().notNull().unique(),
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
expect(validateSchema(schema)).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('reports missing primary key', () => {
|
|
17
|
+
const schema = defineSchema({
|
|
18
|
+
user: table({
|
|
19
|
+
name: column.text().notNull(),
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
const errors = validateSchema(schema);
|
|
23
|
+
expect(errors).toHaveLength(1);
|
|
24
|
+
expect(errors[0].message).toContain('primary key');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reports notNull + nullable conflict', () => {
|
|
28
|
+
const schema = defineSchema({
|
|
29
|
+
user: table({
|
|
30
|
+
id: column.serial().primaryKey(),
|
|
31
|
+
name: column.text().notNull().nullable(),
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
const errors = validateSchema(schema);
|
|
35
|
+
const conflict = errors.find((e) => e.column === 'name');
|
|
36
|
+
expect(conflict).toBeDefined();
|
|
37
|
+
expect(conflict!.message).toContain('notNull and nullable');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('reports foreign key to unknown table', () => {
|
|
41
|
+
const schema = defineSchema({
|
|
42
|
+
post: table({
|
|
43
|
+
id: column.serial().primaryKey(),
|
|
44
|
+
authorId: column.integer().references('user', 'id'),
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
const errors = validateSchema(schema);
|
|
48
|
+
const fkError = errors.find((e) => e.column === 'authorId');
|
|
49
|
+
expect(fkError).toBeDefined();
|
|
50
|
+
expect(fkError!.message).toContain("unknown table 'user'");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reports foreign key to unknown column', () => {
|
|
54
|
+
const schema = defineSchema({
|
|
55
|
+
user: table({
|
|
56
|
+
id: column.serial().primaryKey(),
|
|
57
|
+
}),
|
|
58
|
+
post: table({
|
|
59
|
+
id: column.serial().primaryKey(),
|
|
60
|
+
authorId: column.integer().references('user', 'uid'),
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
const errors = validateSchema(schema);
|
|
64
|
+
const fkError = errors.find((e) => e.column === 'authorId');
|
|
65
|
+
expect(fkError).toBeDefined();
|
|
66
|
+
expect(fkError!.message).toContain("unknown column 'uid'");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reports onDelete without references', () => {
|
|
70
|
+
const schema = defineSchema({
|
|
71
|
+
user: table({
|
|
72
|
+
id: column.serial().primaryKey(),
|
|
73
|
+
val: column.integer().onDelete('cascade'),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const errors = validateSchema(schema);
|
|
77
|
+
expect(errors.some((e) => e.message.includes('onDelete'))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('reports invalid varchar length', () => {
|
|
81
|
+
const schema = defineSchema({
|
|
82
|
+
user: table({
|
|
83
|
+
id: column.serial().primaryKey(),
|
|
84
|
+
name: column.varchar(0),
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
const errors = validateSchema(schema);
|
|
88
|
+
expect(errors.some((e) => e.message.includes('varchar length'))).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reports decimal scale > precision', () => {
|
|
92
|
+
const schema = defineSchema({
|
|
93
|
+
product: table({
|
|
94
|
+
id: column.serial().primaryKey(),
|
|
95
|
+
price: column.decimal(2, 10),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
const errors = validateSchema(schema);
|
|
99
|
+
expect(errors.some((e) => e.message.includes('scale cannot exceed'))).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('valid schema with foreign keys passes', () => {
|
|
103
|
+
const schema = defineSchema({
|
|
104
|
+
user: table({ id: column.serial().primaryKey() }),
|
|
105
|
+
post: table({
|
|
106
|
+
id: column.serial().primaryKey(),
|
|
107
|
+
authorId: column
|
|
108
|
+
.integer()
|
|
109
|
+
.notNull()
|
|
110
|
+
.references('user', 'id')
|
|
111
|
+
.onDelete('cascade'),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
expect(validateSchema(schema)).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED