@axium/server 0.36.6 → 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.
- package/dist/acl.js +1 -1
- package/dist/database.d.ts +11 -507
- package/dist/database.js +8 -557
- package/dist/db/connection.d.ts +5 -0
- package/dist/db/connection.js +18 -0
- package/dist/db/data.d.ts +119 -0
- package/dist/db/data.js +113 -0
- package/dist/db/delta.d.ts +147 -0
- package/dist/db/delta.js +390 -0
- package/dist/db/schema.d.ts +268 -0
- package/dist/db/schema.js +169 -0
- package/dist/{db.json → db/schema.json} +5 -3
- package/dist/main.js +37 -23
- package/package.json +2 -2
- package/routes/admin/users/+page.svelte +1 -1
- package/schemas/db.json +48 -13
package/dist/db/delta.js
ADDED
|
@@ -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
|
+
}
|