@axium/server 0.36.5 → 0.37.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.
@@ -0,0 +1,390 @@
1
+ import * as io from '@axium/core/node/io';
2
+ import { sql } from 'kysely';
3
+ import { styleText } from 'node:util';
4
+ import * as z from 'zod';
5
+ import { database } from './connection.js';
6
+ import * as data from './data.js';
7
+ export const Column = z.strictObject({
8
+ type: data.ColumnType.optional(),
9
+ default: z.string().optional(),
10
+ ops: z.literal(['drop_default', 'set_required', 'drop_required']).array().optional(),
11
+ });
12
+ export const Table = z.strictObject({
13
+ add_columns: z.record(z.string(), data.Column).optional().default({}),
14
+ drop_columns: z.string().array().optional().default([]),
15
+ alter_columns: z.record(z.string(), Column).optional().default({}),
16
+ add_constraints: z.record(z.string(), data.Constraint).optional().default({}),
17
+ drop_constraints: z.string().array().optional().default([]),
18
+ });
19
+ export function applyToTable(table, delta) {
20
+ for (const column of delta.drop_columns) {
21
+ if (column in table.columns)
22
+ delete table.columns[column];
23
+ else
24
+ throw `can't drop column ${column} because it does not exist`;
25
+ }
26
+ for (const [name, column] of Object.entries(delta.add_columns)) {
27
+ if (name in table.columns)
28
+ throw `can't add column ${name} because it already exists`;
29
+ table.columns[name] = column;
30
+ }
31
+ for (const [name, columnDelta] of Object.entries(delta.alter_columns)) {
32
+ const column = table.columns[name];
33
+ if (!column)
34
+ throw `can't modify column ${name} because it does not exist`;
35
+ if (columnDelta.type)
36
+ column.type = columnDelta.type;
37
+ if (columnDelta.default)
38
+ column.default = columnDelta.default;
39
+ for (const op of columnDelta.ops || []) {
40
+ switch (op) {
41
+ case 'drop_default':
42
+ delete column.default;
43
+ break;
44
+ case 'set_required':
45
+ column.required = true;
46
+ break;
47
+ case 'drop_required':
48
+ column.required = false;
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ for (const name of delta.drop_constraints) {
54
+ if (table.constraints[name])
55
+ delete table.constraints[name];
56
+ else
57
+ throw `can't drop constraint ${name} because it does not exist`;
58
+ }
59
+ for (const [name, constraint] of Object.entries(delta.add_constraints)) {
60
+ if (table.constraints[name])
61
+ throw `can't add constraint ${name} because it already exists`;
62
+ table.constraints[name] = constraint;
63
+ }
64
+ }
65
+ export const Version = z.strictObject({
66
+ delta: z.literal(true),
67
+ add_tables: z.record(z.string(), data.Table).optional().default({}),
68
+ drop_tables: z.string().array().optional().default([]),
69
+ alter_tables: z.record(z.string(), Table).optional().default({}),
70
+ add_indexes: z.record(z.string(), data.Index).optional().default({}),
71
+ drop_indexes: z.string().array().optional().default([]),
72
+ });
73
+ export function applyToSchema(schema, delta) {
74
+ for (const tableName of delta.drop_tables) {
75
+ if (tableName in schema.tables)
76
+ delete schema.tables[tableName];
77
+ else
78
+ throw `can't drop table ${tableName} because it does not exist`;
79
+ }
80
+ for (const [tableName, table] of Object.entries(delta.add_tables)) {
81
+ if (tableName in schema.tables)
82
+ throw `can't add table ${tableName} because it already exists`;
83
+ else
84
+ schema.tables[tableName] = table;
85
+ }
86
+ for (const [tableName, tableDelta] of Object.entries(delta.alter_tables)) {
87
+ if (tableName in schema.tables)
88
+ applyToTable(schema.tables[tableName], tableDelta);
89
+ else
90
+ throw `can't modify table ${tableName} because it does not exist`;
91
+ }
92
+ for (const indexName of delta.drop_indexes) {
93
+ if (indexName in schema.indexes)
94
+ delete schema.indexes[indexName];
95
+ else
96
+ throw `can't drop index ${indexName} because it does not exist`;
97
+ }
98
+ for (const [indexName, index] of Object.entries(delta.add_indexes)) {
99
+ if (indexName in schema.indexes)
100
+ throw `can't add index ${indexName} because it already exists`;
101
+ else
102
+ schema.indexes[indexName] = index;
103
+ }
104
+ }
105
+ export function validate(delta) {
106
+ const tableNames = [...Object.keys(delta.add_tables), ...Object.keys(delta.alter_tables), delta.drop_tables];
107
+ const uniqueTables = new Set(tableNames);
108
+ for (const table of uniqueTables) {
109
+ tableNames.splice(tableNames.indexOf(table), 1);
110
+ }
111
+ if (tableNames.length) {
112
+ throw `Duplicate table name(s): ${tableNames.join(', ')}`;
113
+ }
114
+ for (const [tableName, table] of Object.entries(delta.alter_tables)) {
115
+ const columnNames = [...Object.keys(table.add_columns), ...table.drop_columns];
116
+ const uniqueColumns = new Set(columnNames);
117
+ for (const column of uniqueColumns) {
118
+ columnNames.splice(columnNames.indexOf(column), 1);
119
+ }
120
+ if (columnNames.length) {
121
+ throw `Duplicate column name(s) in table ${tableName}: ${columnNames.join(', ')}`;
122
+ }
123
+ }
124
+ }
125
+ export function compute(from, to) {
126
+ const fromTables = new Set(Object.keys(from.tables));
127
+ const toTables = new Set(Object.keys(to.tables));
128
+ const fromIndexes = new Set(Object.keys(from.indexes));
129
+ const toIndexes = new Set(Object.keys(to.indexes));
130
+ const add_tables = Object.fromEntries(toTables
131
+ .difference(fromTables)
132
+ .keys()
133
+ .map(name => [name, to.tables[name]]));
134
+ const alter_tables = {};
135
+ for (const name of fromTables.intersection(toTables)) {
136
+ const fromTable = from.tables[name], toTable = to.tables[name];
137
+ const fromColumns = new Set(Object.keys(fromTable));
138
+ const toColumns = new Set(Object.keys(toTable));
139
+ const drop_columns = fromColumns.difference(toColumns);
140
+ const add_columns = Object.fromEntries(toColumns
141
+ .difference(fromColumns)
142
+ .keys()
143
+ .map(colName => [colName, toTable.columns[colName]]));
144
+ const alter_columns = Object.fromEntries(toColumns
145
+ .intersection(fromColumns)
146
+ .keys()
147
+ .map(name => {
148
+ const fromCol = fromTable.columns[name], toCol = toTable.columns[name];
149
+ const alter = { ops: [] };
150
+ if ('default' in fromCol && !('default' in toCol))
151
+ alter.ops.push('drop_default');
152
+ else if (fromCol.default !== toCol.default)
153
+ alter.default = toCol.default;
154
+ if (fromCol.type != toCol.type)
155
+ alter.type = toCol.type;
156
+ if (fromCol.required != toCol.required)
157
+ alter.ops.push(toCol.required ? 'set_required' : 'drop_required');
158
+ return [name, alter];
159
+ }));
160
+ const fromConstraints = new Set(Object.keys(fromTable.constraints || {}));
161
+ const toConstraints = new Set(Object.keys(toTable.constraints || {}));
162
+ const drop_constraints = fromConstraints.difference(toConstraints);
163
+ const add_constraints = Object.fromEntries(toConstraints
164
+ .difference(fromConstraints)
165
+ .keys()
166
+ .map(constName => [constName, toTable.constraints[constName]]));
167
+ alter_tables[name] = {
168
+ add_columns,
169
+ drop_columns: Array.from(drop_columns),
170
+ alter_columns,
171
+ add_constraints,
172
+ drop_constraints: Array.from(drop_constraints),
173
+ };
174
+ }
175
+ const add_indexes = Object.fromEntries(toIndexes
176
+ .difference(fromIndexes)
177
+ .keys()
178
+ .map(name => [name, to.indexes[name]]));
179
+ return {
180
+ delta: true,
181
+ add_tables,
182
+ drop_tables: Array.from(fromTables.difference(toTables)),
183
+ alter_tables,
184
+ drop_indexes: Array.from(fromIndexes.difference(toIndexes)),
185
+ add_indexes,
186
+ };
187
+ }
188
+ export function collapse(deltas) {
189
+ const add_tables = {}, drop_tables = [], alter_tables = {}, add_indexes = {}, drop_indexes = [];
190
+ for (const delta of deltas) {
191
+ validate(delta);
192
+ for (const [name, table] of Object.entries(delta.alter_tables)) {
193
+ if (name in add_tables) {
194
+ applyToTable(add_tables[name], table);
195
+ }
196
+ else if (name in alter_tables) {
197
+ const existing = alter_tables[name];
198
+ for (const [colName, column] of Object.entries(table.add_columns)) {
199
+ existing.add_columns[colName] = column;
200
+ }
201
+ for (const colName of table.drop_columns) {
202
+ if (colName in existing.add_columns)
203
+ delete existing.add_columns[colName];
204
+ else
205
+ existing.drop_columns.push(colName);
206
+ }
207
+ }
208
+ else
209
+ alter_tables[name] = table;
210
+ }
211
+ for (const table of delta.drop_tables) {
212
+ if (table in add_tables)
213
+ delete add_tables[table];
214
+ else
215
+ drop_tables.push(table);
216
+ }
217
+ for (const index of delta.drop_indexes) {
218
+ if (index in add_indexes)
219
+ delete add_indexes[index];
220
+ else
221
+ drop_indexes.push(index);
222
+ }
223
+ for (const [name, table] of Object.entries(delta.add_tables)) {
224
+ if (drop_tables.includes(name))
225
+ throw `Can't add and drop table "${name}" in the same change`;
226
+ if (name in alter_tables)
227
+ throw `Can't add and modify table "${name}" in the same change`;
228
+ add_tables[name] = table;
229
+ }
230
+ for (const [name, index] of Object.entries(delta.add_indexes)) {
231
+ if (drop_indexes.includes(name))
232
+ throw `Can't add and drop index "${name}" in the same change`;
233
+ add_indexes[name] = index;
234
+ }
235
+ }
236
+ return { delta: true, add_tables, drop_tables, alter_tables, add_indexes, drop_indexes };
237
+ }
238
+ export function isEmpty(delta) {
239
+ return (!Object.keys(delta.add_tables).length &&
240
+ !delta.drop_tables.length &&
241
+ !Object.keys(delta.alter_tables).length &&
242
+ !Object.keys(delta.add_indexes).length &&
243
+ !delta.drop_indexes.length);
244
+ }
245
+ const deltaColors = {
246
+ '+': 'green',
247
+ '-': 'red',
248
+ '*': 'white',
249
+ };
250
+ export function* display(delta) {
251
+ const tables = [
252
+ ...Object.keys(delta.add_tables).map(name => ({ op: '+', name })),
253
+ ...Object.entries(delta.alter_tables).map(([name, changes]) => ({ op: '*', name, changes })),
254
+ ...delta.drop_tables.map(name => ({ op: '-', name })),
255
+ ];
256
+ tables.sort((a, b) => a.name.localeCompare(b.name));
257
+ for (const table of tables) {
258
+ yield styleText(deltaColors[table.op], `${table.op} table ${table.name}`);
259
+ if (table.op != '*')
260
+ continue;
261
+ const columns = [
262
+ ...Object.keys(table.changes.add_columns).map(name => ({ op: '+', name })),
263
+ ...table.changes.drop_columns.map(name => ({ op: '-', name })),
264
+ ...Object.entries(table.changes.alter_columns).map(([name, changes]) => ({ op: '*', name, ...changes })),
265
+ ];
266
+ columns.sort((a, b) => a.name.localeCompare(b.name));
267
+ for (const column of columns) {
268
+ const columnChanges = column.op == '*'
269
+ ? [...(column.ops ?? []), 'default' in column && 'set_default', 'type' in column && 'set_type']
270
+ .filter((e) => !!e)
271
+ .map(e => e.replaceAll('_', ' '))
272
+ .join(', ')
273
+ : null;
274
+ yield '\t' +
275
+ styleText(deltaColors[column.op], `${column.op} column ${column.name}${column.op != '*' ? '' : ': ' + columnChanges}`);
276
+ }
277
+ const constraints = [
278
+ ...Object.keys(table.changes.add_constraints).map(name => ({ op: '+', name })),
279
+ ...table.changes.drop_constraints.map(name => ({ op: '-', name })),
280
+ ];
281
+ for (const con of constraints) {
282
+ yield '\t' + styleText(deltaColors[con.op], `${con.op} constraint ${con.name}`);
283
+ }
284
+ }
285
+ const indexes = [
286
+ ...Object.keys(delta.add_indexes).map(name => ({ op: '+', name })),
287
+ ...delta.drop_indexes.map(name => ({ op: '-', name })),
288
+ ];
289
+ indexes.sort((a, b) => a.name.localeCompare(b.name));
290
+ for (const index of indexes) {
291
+ yield styleText(deltaColors[index.op], `${index.op} index ${index.name}`);
292
+ }
293
+ }
294
+ export async function apply(delta, forceAbort = false) {
295
+ const tx = await database.startTransaction().execute();
296
+ try {
297
+ for (const [tableName, table] of Object.entries(delta.add_tables)) {
298
+ io.start('Adding table ' + tableName);
299
+ let query = tx.schema.createTable(tableName);
300
+ const columns = Object.entries(table.columns);
301
+ const pkColumns = columns.filter(([, column]) => column.primary).map(([name, column]) => ({ name, ...column }));
302
+ const needsSpecialConstraint = pkColumns.length > 1 || pkColumns.some(col => !col.required);
303
+ for (const [colName, column] of columns) {
304
+ query = query.addColumn(colName, sql.raw(column.type), data.buildColumn(column, !needsSpecialConstraint));
305
+ }
306
+ if (needsSpecialConstraint) {
307
+ query = query.addPrimaryKeyConstraint('PK_' + tableName.replaceAll('.', '_'), pkColumns.map(col => col.name));
308
+ }
309
+ await query.execute();
310
+ io.done();
311
+ }
312
+ for (const tableName of delta.drop_tables) {
313
+ io.start('Dropping table ' + tableName);
314
+ await tx.schema.dropTable(tableName).execute();
315
+ io.done();
316
+ }
317
+ for (const [tableName, tableDelta] of Object.entries(delta.alter_tables)) {
318
+ io.start(`Modifying table ${tableName}`);
319
+ const query = tx.schema.alterTable(tableName);
320
+ for (const constraint of tableDelta.drop_constraints) {
321
+ await query.dropConstraint(constraint).execute();
322
+ }
323
+ for (const colName of tableDelta.drop_columns) {
324
+ await query.dropColumn(colName).execute();
325
+ }
326
+ for (const [colName, column] of Object.entries(tableDelta.add_columns)) {
327
+ await query.addColumn(colName, sql.raw(column.type), data.buildColumn(column, false)).execute();
328
+ }
329
+ for (const [colName, column] of Object.entries(tableDelta.alter_columns)) {
330
+ if (column.default)
331
+ await query.alterColumn(colName, col => col.setDefault(sql.raw(String(column.default)))).execute();
332
+ if (column.type)
333
+ await query.alterColumn(colName, col => col.setDataType(sql.raw(column.type))).execute();
334
+ for (const op of column.ops ?? []) {
335
+ switch (op) {
336
+ case 'drop_default':
337
+ if (column.default)
338
+ throw 'Cannot set and drop default at the same time';
339
+ await query.alterColumn(colName, col => col.dropDefault()).execute();
340
+ break;
341
+ case 'set_required':
342
+ await query.alterColumn(colName, col => col.setNotNull()).execute();
343
+ break;
344
+ case 'drop_required':
345
+ await query.alterColumn(colName, col => col.dropNotNull()).execute();
346
+ break;
347
+ }
348
+ }
349
+ }
350
+ for (const [name, con] of Object.entries(tableDelta.add_constraints)) {
351
+ switch (con.type) {
352
+ case 'unique':
353
+ await query.addUniqueConstraint(name, con.on, b => (con.nulls_not_distinct ? b.nullsNotDistinct() : b)).execute();
354
+ break;
355
+ case 'check':
356
+ await query.addCheckConstraint(name, sql.raw(con.check)).execute();
357
+ break;
358
+ case 'foreign_key':
359
+ await query.addForeignKeyConstraint(name, con.on, con.target, con.references, b => b).execute();
360
+ break;
361
+ case 'primary_key':
362
+ await query.addPrimaryKeyConstraint(name, con.on).execute();
363
+ break;
364
+ }
365
+ }
366
+ io.done();
367
+ }
368
+ for (const [indexName, index] of Object.entries(delta.add_indexes)) {
369
+ io.start('Adding index ' + indexName);
370
+ await tx.schema.createIndex(indexName).on(index.on).columns(index.columns).execute();
371
+ io.done();
372
+ }
373
+ for (const index of delta.drop_indexes) {
374
+ io.start('Dropping index ' + index);
375
+ await tx.schema.dropIndex(index).execute();
376
+ io.done();
377
+ }
378
+ if (forceAbort)
379
+ throw 'Rolling back due to --abort';
380
+ io.start('Committing');
381
+ await tx.commit().execute();
382
+ io.done();
383
+ }
384
+ catch (e) {
385
+ await tx.rollback().execute();
386
+ if (e instanceof SuppressedError)
387
+ io.error(e.suppressed);
388
+ throw e;
389
+ }
390
+ }