@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.
- 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 +3 -3
- package/routes/admin/users/+page.svelte +3 -3
- package/schemas/db.json +48 -13
package/dist/database.js
CHANGED
|
@@ -52,36 +52,26 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
52
52
|
});
|
|
53
53
|
import * as io from '@axium/core/node/io';
|
|
54
54
|
import { plugins } from '@axium/core/plugins';
|
|
55
|
-
import {
|
|
55
|
+
import { sql } from 'kysely';
|
|
56
56
|
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
57
57
|
import { randomBytes } from 'node:crypto';
|
|
58
58
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
59
|
-
import { join } from 'node:path
|
|
59
|
+
import { join } from 'node:path';
|
|
60
60
|
import { styleText } from 'node:util';
|
|
61
61
|
import pg from 'pg';
|
|
62
62
|
import * as z from 'zod';
|
|
63
63
|
import config from './config.js';
|
|
64
|
-
import rawSchema from './db.json' with { type: 'json' };
|
|
65
64
|
import { dirs, systemDir } from './io.js';
|
|
65
|
+
import { connect, database } from './db/connection.js';
|
|
66
|
+
export { connect, database };
|
|
67
|
+
import * as schema from './db/schema.js';
|
|
68
|
+
export * as delta from './db/delta.js';
|
|
69
|
+
export * as schema from './db/schema.js';
|
|
66
70
|
pg.types.setTypeParser(pg.types.builtins.INT8, BigInt);
|
|
67
71
|
// @ts-expect-error 2339
|
|
68
72
|
BigInt.prototype.toJSON = function () {
|
|
69
73
|
return Number(this);
|
|
70
74
|
};
|
|
71
|
-
const sym = Symbol.for('Axium:database');
|
|
72
|
-
export let database;
|
|
73
|
-
export function connect() {
|
|
74
|
-
if (database)
|
|
75
|
-
return database;
|
|
76
|
-
if (globalThis[sym])
|
|
77
|
-
return (database = globalThis[sym]);
|
|
78
|
-
database = new Kysely({
|
|
79
|
-
dialect: new PostgresDialect({ pool: new pg.Pool(config.db) }),
|
|
80
|
-
});
|
|
81
|
-
globalThis[sym] = database;
|
|
82
|
-
io.debug('Connected to database!');
|
|
83
|
-
return database;
|
|
84
|
-
}
|
|
85
75
|
// Helpers
|
|
86
76
|
export async function count(...tables) {
|
|
87
77
|
return await database
|
|
@@ -235,187 +225,6 @@ export async function init(opt) {
|
|
|
235
225
|
await result_1;
|
|
236
226
|
}
|
|
237
227
|
}
|
|
238
|
-
const numberTypes = [
|
|
239
|
-
'integer',
|
|
240
|
-
'int2',
|
|
241
|
-
'int4',
|
|
242
|
-
'int8',
|
|
243
|
-
'smallint',
|
|
244
|
-
'real',
|
|
245
|
-
'double precision',
|
|
246
|
-
'float4',
|
|
247
|
-
'float8',
|
|
248
|
-
'decimal',
|
|
249
|
-
'numeric',
|
|
250
|
-
'serial',
|
|
251
|
-
];
|
|
252
|
-
const bigintTypes = ['bigint', 'bigserial'];
|
|
253
|
-
const booleanTypes = ['boolean', 'bool'];
|
|
254
|
-
const stringTypes = ['varchar', 'char', 'text'];
|
|
255
|
-
const dateTypes = ['date', 'datetime', 'time', 'timetz', 'timestamp', 'timestamptz'];
|
|
256
|
-
const binaryTypes = ['binary', 'bytea', 'varbinary', 'blob'];
|
|
257
|
-
const numericRangeTypes = ['int4range', 'numrange'];
|
|
258
|
-
const stringRangeTypes = ['tsrange', 'tstzrange', 'daterange'];
|
|
259
|
-
const multirangeTypes = ['int4multirange', 'int8multirange', 'nummultirange', 'tsmultirange', 'tstzmultirange', 'datemultirange'];
|
|
260
|
-
const _primitive = z.literal([
|
|
261
|
-
...numberTypes,
|
|
262
|
-
...bigintTypes,
|
|
263
|
-
...booleanTypes,
|
|
264
|
-
...stringTypes,
|
|
265
|
-
...dateTypes,
|
|
266
|
-
...binaryTypes,
|
|
267
|
-
...numericRangeTypes,
|
|
268
|
-
...stringRangeTypes,
|
|
269
|
-
...multirangeTypes,
|
|
270
|
-
'uuid',
|
|
271
|
-
'json',
|
|
272
|
-
'jsonb',
|
|
273
|
-
]);
|
|
274
|
-
const _ColumnType = z.union([
|
|
275
|
-
_primitive,
|
|
276
|
-
z.templateLiteral([
|
|
277
|
-
z.literal(['char', 'varchar', 'binary', 'varbinary', 'datetime', 'time', 'timetz', 'timestamp', 'timestamptz']),
|
|
278
|
-
'(',
|
|
279
|
-
z.int().nonnegative(),
|
|
280
|
-
')',
|
|
281
|
-
]),
|
|
282
|
-
z.templateLiteral([z.literal(['decimal', 'numeric']), '(', z.int().nonnegative(), z.literal([',', ', ']), z.int().nonnegative(), ')']),
|
|
283
|
-
]);
|
|
284
|
-
const ColumnType = z.union([_ColumnType, z.templateLiteral([_ColumnType, '[', z.int().nonnegative().optional(), ']'])]);
|
|
285
|
-
export const Column = z.strictObject({
|
|
286
|
-
type: ColumnType,
|
|
287
|
-
required: z.boolean().default(false),
|
|
288
|
-
unique: z.boolean().default(false),
|
|
289
|
-
primary: z.boolean().default(false),
|
|
290
|
-
references: z.string().optional(),
|
|
291
|
-
onDelete: z.enum(['cascade', 'restrict', 'no action', 'set null', 'set default']).optional(),
|
|
292
|
-
default: z.any().optional(),
|
|
293
|
-
check: z.string().optional(),
|
|
294
|
-
});
|
|
295
|
-
export const Constraint = z.discriminatedUnion('type', [
|
|
296
|
-
z.strictObject({
|
|
297
|
-
type: z.literal('primary_key'),
|
|
298
|
-
on: z.string().array(),
|
|
299
|
-
}),
|
|
300
|
-
z.strictObject({
|
|
301
|
-
type: z.literal('foreign_key'),
|
|
302
|
-
on: z.string().array(),
|
|
303
|
-
target: z.string(),
|
|
304
|
-
references: z.string().array(),
|
|
305
|
-
}),
|
|
306
|
-
z.strictObject({
|
|
307
|
-
type: z.literal('unique'),
|
|
308
|
-
on: z.string().array(),
|
|
309
|
-
nulls_not_distinct: z.boolean().optional(),
|
|
310
|
-
}),
|
|
311
|
-
z.strictObject({
|
|
312
|
-
type: z.literal('check'),
|
|
313
|
-
check: z.string(),
|
|
314
|
-
}),
|
|
315
|
-
]);
|
|
316
|
-
export const Table = z.strictObject({
|
|
317
|
-
columns: z.record(z.string(), Column),
|
|
318
|
-
constraints: z.record(z.string(), Constraint).optional().default({}),
|
|
319
|
-
});
|
|
320
|
-
export const IndexString = z.templateLiteral([z.string(), ':', z.string()]);
|
|
321
|
-
export function parseIndex(value) {
|
|
322
|
-
const [table, column] = value.split(':');
|
|
323
|
-
return { table, column };
|
|
324
|
-
}
|
|
325
|
-
export const SchemaDecl = z.strictObject({
|
|
326
|
-
tables: z.record(z.string(), Table),
|
|
327
|
-
indexes: IndexString.array().optional().default([]),
|
|
328
|
-
});
|
|
329
|
-
export const ColumnDelta = z.strictObject({
|
|
330
|
-
type: ColumnType.optional(),
|
|
331
|
-
default: z.string().optional(),
|
|
332
|
-
ops: z.literal(['drop_default', 'set_required', 'drop_required']).array().optional(),
|
|
333
|
-
});
|
|
334
|
-
export const TableDelta = z.strictObject({
|
|
335
|
-
add_columns: z.record(z.string(), Column).optional().default({}),
|
|
336
|
-
drop_columns: z.string().array().optional().default([]),
|
|
337
|
-
alter_columns: z.record(z.string(), ColumnDelta).optional().default({}),
|
|
338
|
-
add_constraints: z.record(z.string(), Constraint).optional().default({}),
|
|
339
|
-
drop_constraints: z.string().array().optional().default([]),
|
|
340
|
-
});
|
|
341
|
-
export const VersionDelta = z.strictObject({
|
|
342
|
-
delta: z.literal(true),
|
|
343
|
-
add_tables: z.record(z.string(), Table).optional().default({}),
|
|
344
|
-
drop_tables: z.string().array().optional().default([]),
|
|
345
|
-
alter_tables: z.record(z.string(), TableDelta).optional().default({}),
|
|
346
|
-
add_indexes: IndexString.array().optional().default([]),
|
|
347
|
-
drop_indexes: IndexString.array().optional().default([]),
|
|
348
|
-
});
|
|
349
|
-
export const SchemaFile = z.object({
|
|
350
|
-
format: z.literal(0),
|
|
351
|
-
versions: z.discriminatedUnion('delta', [SchemaDecl.extend({ delta: z.literal(false) }), VersionDelta]).array(),
|
|
352
|
-
/** List of tables to wipe */
|
|
353
|
-
wipe: z.string().array().optional().default([]),
|
|
354
|
-
/** Set the latest version, defaults to the last one */
|
|
355
|
-
latest: z.int32().nonnegative().optional(),
|
|
356
|
-
/** Maps tables to their ACL tables, e.g. `"storage": "acl.storage"` */
|
|
357
|
-
acl_tables: z.record(z.string(), z.string()).optional().default({}),
|
|
358
|
-
});
|
|
359
|
-
const { data, error } = SchemaFile.safeParse(rawSchema);
|
|
360
|
-
if (error)
|
|
361
|
-
io.error('Invalid base database schema:\n' + z.prettifyError(error));
|
|
362
|
-
const schema = data;
|
|
363
|
-
export function* getSchemaFiles() {
|
|
364
|
-
yield ['@axium/server', schema];
|
|
365
|
-
for (const [name, plugin] of plugins) {
|
|
366
|
-
if (!plugin._db)
|
|
367
|
-
continue;
|
|
368
|
-
try {
|
|
369
|
-
yield [name, SchemaFile.parse(plugin._db)];
|
|
370
|
-
}
|
|
371
|
-
catch (e) {
|
|
372
|
-
throw `Invalid database configuration for plugin "${name}":\n${io.errorText(e)}`;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Get the active schema
|
|
378
|
-
*/
|
|
379
|
-
export function getFullSchema(opt = {}) {
|
|
380
|
-
const fullSchema = { tables: {}, indexes: [], versions: {} };
|
|
381
|
-
for (const [pluginName, file] of getSchemaFiles()) {
|
|
382
|
-
if (opt.exclude?.includes(pluginName))
|
|
383
|
-
continue;
|
|
384
|
-
file.latest ??= file.versions.length - 1;
|
|
385
|
-
let currentSchema = { tables: {}, indexes: [] };
|
|
386
|
-
fullSchema.versions[pluginName] = file.latest;
|
|
387
|
-
for (const [version, schema] of file.versions.entries()) {
|
|
388
|
-
if (!schema.delta)
|
|
389
|
-
currentSchema = schema;
|
|
390
|
-
else {
|
|
391
|
-
try {
|
|
392
|
-
applyDeltaToSchema(currentSchema, schema);
|
|
393
|
-
}
|
|
394
|
-
catch (e) {
|
|
395
|
-
throw `Failed to apply version ${version - 1}->${version} delta to ${pluginName}: ${io.errorText(e)}`;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (version === file.latest)
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
for (const name of Object.keys(currentSchema.tables)) {
|
|
402
|
-
if (name in fullSchema.tables)
|
|
403
|
-
throw 'Duplicate table name in database schema: ' + name;
|
|
404
|
-
fullSchema.tables[name] = currentSchema.tables[name];
|
|
405
|
-
}
|
|
406
|
-
for (const index of currentSchema.indexes) {
|
|
407
|
-
if (fullSchema.indexes.includes(index))
|
|
408
|
-
throw 'Duplicate index in database schema: ' + index;
|
|
409
|
-
fullSchema.indexes.push(index);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return fullSchema;
|
|
413
|
-
}
|
|
414
|
-
const schemaToIntrospected = {
|
|
415
|
-
boolean: 'bool',
|
|
416
|
-
integer: 'int4',
|
|
417
|
-
'text[]': '_text',
|
|
418
|
-
};
|
|
419
228
|
const VersionMap = z.record(z.string(), z.int32().nonnegative());
|
|
420
229
|
export const UpgradesInfo = z.object({
|
|
421
230
|
current: VersionMap.default({}),
|
|
@@ -430,364 +239,6 @@ export function getUpgradeInfo() {
|
|
|
430
239
|
export function setUpgradeInfo(info) {
|
|
431
240
|
io.writeJSON(upgradesFilePath, info);
|
|
432
241
|
}
|
|
433
|
-
export function applyTableDeltaToSchema(table, delta) {
|
|
434
|
-
for (const column of delta.drop_columns) {
|
|
435
|
-
if (column in table.columns)
|
|
436
|
-
delete table.columns[column];
|
|
437
|
-
else
|
|
438
|
-
throw `can't drop column ${column} because it does not exist`;
|
|
439
|
-
}
|
|
440
|
-
for (const [name, column] of Object.entries(delta.add_columns)) {
|
|
441
|
-
if (name in table.columns)
|
|
442
|
-
throw `can't add column ${name} because it already exists`;
|
|
443
|
-
table.columns[name] = column;
|
|
444
|
-
}
|
|
445
|
-
for (const [name, columnDelta] of Object.entries(delta.alter_columns)) {
|
|
446
|
-
const column = table.columns[name];
|
|
447
|
-
if (!column)
|
|
448
|
-
throw `can't modify column ${name} because it does not exist`;
|
|
449
|
-
if (columnDelta.type)
|
|
450
|
-
column.type = columnDelta.type;
|
|
451
|
-
if (columnDelta.default)
|
|
452
|
-
column.default = columnDelta.default;
|
|
453
|
-
for (const op of columnDelta.ops || []) {
|
|
454
|
-
switch (op) {
|
|
455
|
-
case 'drop_default':
|
|
456
|
-
delete column.default;
|
|
457
|
-
break;
|
|
458
|
-
case 'set_required':
|
|
459
|
-
column.required = true;
|
|
460
|
-
break;
|
|
461
|
-
case 'drop_required':
|
|
462
|
-
column.required = false;
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
for (const name of delta.drop_constraints) {
|
|
468
|
-
if (table.constraints[name])
|
|
469
|
-
delete table.constraints[name];
|
|
470
|
-
else
|
|
471
|
-
throw `can't drop constraint ${name} because it does not exist`;
|
|
472
|
-
}
|
|
473
|
-
for (const [name, constraint] of Object.entries(delta.add_constraints)) {
|
|
474
|
-
if (table.constraints[name])
|
|
475
|
-
throw `can't add constraint ${name} because it already exists`;
|
|
476
|
-
table.constraints[name] = constraint;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
export function applyDeltaToSchema(schema, delta) {
|
|
480
|
-
for (const tableName of delta.drop_tables) {
|
|
481
|
-
if (tableName in schema.tables)
|
|
482
|
-
delete schema.tables[tableName];
|
|
483
|
-
else
|
|
484
|
-
throw `can't drop table ${tableName} because it does not exist`;
|
|
485
|
-
}
|
|
486
|
-
for (const [tableName, table] of Object.entries(delta.add_tables)) {
|
|
487
|
-
if (tableName in schema.tables)
|
|
488
|
-
throw `can't add table ${tableName} because it already exists`;
|
|
489
|
-
else
|
|
490
|
-
schema.tables[tableName] = table;
|
|
491
|
-
}
|
|
492
|
-
for (const [tableName, tableDelta] of Object.entries(delta.alter_tables)) {
|
|
493
|
-
if (tableName in schema.tables)
|
|
494
|
-
applyTableDeltaToSchema(schema.tables[tableName], tableDelta);
|
|
495
|
-
else
|
|
496
|
-
throw `can't modify table ${tableName} because it does not exist`;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
export function validateDelta(delta) {
|
|
500
|
-
const tableNames = [...Object.keys(delta.add_tables), ...Object.keys(delta.alter_tables), delta.drop_tables];
|
|
501
|
-
const uniqueTables = new Set(tableNames);
|
|
502
|
-
for (const table of uniqueTables) {
|
|
503
|
-
tableNames.splice(tableNames.indexOf(table), 1);
|
|
504
|
-
}
|
|
505
|
-
if (tableNames.length) {
|
|
506
|
-
throw `Duplicate table name(s): ${tableNames.join(', ')}`;
|
|
507
|
-
}
|
|
508
|
-
for (const [tableName, table] of Object.entries(delta.alter_tables)) {
|
|
509
|
-
const columnNames = [...Object.keys(table.add_columns), ...table.drop_columns];
|
|
510
|
-
const uniqueColumns = new Set(columnNames);
|
|
511
|
-
for (const column of uniqueColumns) {
|
|
512
|
-
columnNames.splice(columnNames.indexOf(column), 1);
|
|
513
|
-
}
|
|
514
|
-
if (columnNames.length) {
|
|
515
|
-
throw `Duplicate column name(s) in table ${tableName}: ${columnNames.join(', ')}`;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
export function computeDelta(from, to) {
|
|
520
|
-
const fromTables = new Set(Object.keys(from.tables));
|
|
521
|
-
const toTables = new Set(Object.keys(to.tables));
|
|
522
|
-
const fromIndexes = new Set(from.indexes);
|
|
523
|
-
const toIndexes = new Set(to.indexes);
|
|
524
|
-
const add_tables = Object.fromEntries(toTables
|
|
525
|
-
.difference(fromTables)
|
|
526
|
-
.keys()
|
|
527
|
-
.map(name => [name, to.tables[name]]));
|
|
528
|
-
const alter_tables = {};
|
|
529
|
-
for (const name of fromTables.intersection(toTables)) {
|
|
530
|
-
const fromTable = from.tables[name], toTable = to.tables[name];
|
|
531
|
-
const fromColumns = new Set(Object.keys(fromTable));
|
|
532
|
-
const toColumns = new Set(Object.keys(toTable));
|
|
533
|
-
const drop_columns = fromColumns.difference(toColumns);
|
|
534
|
-
const add_columns = Object.fromEntries(toColumns
|
|
535
|
-
.difference(fromColumns)
|
|
536
|
-
.keys()
|
|
537
|
-
.map(colName => [colName, toTable.columns[colName]]));
|
|
538
|
-
const alter_columns = Object.fromEntries(toColumns
|
|
539
|
-
.intersection(fromColumns)
|
|
540
|
-
.keys()
|
|
541
|
-
.map(name => {
|
|
542
|
-
const fromCol = fromTable.columns[name], toCol = toTable.columns[name];
|
|
543
|
-
const alter = { ops: [] };
|
|
544
|
-
if ('default' in fromCol && !('default' in toCol))
|
|
545
|
-
alter.ops.push('drop_default');
|
|
546
|
-
else if (fromCol.default !== toCol.default)
|
|
547
|
-
alter.default = toCol.default;
|
|
548
|
-
if (fromCol.type != toCol.type)
|
|
549
|
-
alter.type = toCol.type;
|
|
550
|
-
if (fromCol.required != toCol.required)
|
|
551
|
-
alter.ops.push(toCol.required ? 'set_required' : 'drop_required');
|
|
552
|
-
return [name, alter];
|
|
553
|
-
}));
|
|
554
|
-
const fromConstraints = new Set(Object.keys(fromTable.constraints || {}));
|
|
555
|
-
const toConstraints = new Set(Object.keys(toTable.constraints || {}));
|
|
556
|
-
const drop_constraints = fromConstraints.difference(toConstraints);
|
|
557
|
-
const add_constraints = Object.fromEntries(toConstraints
|
|
558
|
-
.difference(fromConstraints)
|
|
559
|
-
.keys()
|
|
560
|
-
.map(constName => [constName, toTable.constraints[constName]]));
|
|
561
|
-
alter_tables[name] = {
|
|
562
|
-
add_columns,
|
|
563
|
-
drop_columns: Array.from(drop_columns),
|
|
564
|
-
alter_columns,
|
|
565
|
-
add_constraints,
|
|
566
|
-
drop_constraints: Array.from(drop_constraints),
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
return {
|
|
570
|
-
delta: true,
|
|
571
|
-
add_tables,
|
|
572
|
-
drop_tables: Array.from(fromTables.difference(toTables)),
|
|
573
|
-
alter_tables,
|
|
574
|
-
drop_indexes: Array.from(fromIndexes.difference(toIndexes)),
|
|
575
|
-
add_indexes: Array.from(toIndexes.difference(fromIndexes)),
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
export function collapseDeltas(deltas) {
|
|
579
|
-
const add_tables = {}, drop_tables = [], alter_tables = {}, add_indexes = [], drop_indexes = [];
|
|
580
|
-
for (const delta of deltas) {
|
|
581
|
-
validateDelta(delta);
|
|
582
|
-
for (const [name, table] of Object.entries(delta.alter_tables)) {
|
|
583
|
-
if (name in add_tables) {
|
|
584
|
-
applyTableDeltaToSchema(add_tables[name], table);
|
|
585
|
-
}
|
|
586
|
-
else if (name in alter_tables) {
|
|
587
|
-
const existing = alter_tables[name];
|
|
588
|
-
for (const [colName, column] of Object.entries(table.add_columns)) {
|
|
589
|
-
existing.add_columns[colName] = column;
|
|
590
|
-
}
|
|
591
|
-
for (const colName of table.drop_columns) {
|
|
592
|
-
if (colName in existing.add_columns)
|
|
593
|
-
delete existing.add_columns[colName];
|
|
594
|
-
else
|
|
595
|
-
existing.drop_columns.push(colName);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
else
|
|
599
|
-
alter_tables[name] = table;
|
|
600
|
-
}
|
|
601
|
-
for (const table of delta.drop_tables) {
|
|
602
|
-
if (table in add_tables)
|
|
603
|
-
delete add_tables[table];
|
|
604
|
-
else
|
|
605
|
-
drop_tables.push(table);
|
|
606
|
-
}
|
|
607
|
-
for (const [name, table] of Object.entries(delta.add_tables)) {
|
|
608
|
-
if (drop_tables.includes(name))
|
|
609
|
-
throw `Can't add and drop table "${name}" in the same change`;
|
|
610
|
-
if (name in alter_tables)
|
|
611
|
-
throw `Can't add and modify table "${name}" in the same change`;
|
|
612
|
-
add_tables[name] = table;
|
|
613
|
-
}
|
|
614
|
-
for (const index of delta.add_indexes) {
|
|
615
|
-
if (drop_indexes.includes(index))
|
|
616
|
-
throw `Can't add and drop index "${index}" in the same change`;
|
|
617
|
-
add_indexes.push(index);
|
|
618
|
-
}
|
|
619
|
-
for (const index of delta.drop_indexes) {
|
|
620
|
-
if (add_indexes.includes(index))
|
|
621
|
-
throw `Can't add and drop index "${index}" in the same change`;
|
|
622
|
-
drop_indexes.push(index);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
return { delta: true, add_tables, drop_tables, alter_tables, add_indexes, drop_indexes };
|
|
626
|
-
}
|
|
627
|
-
export function deltaIsEmpty(delta) {
|
|
628
|
-
return (!Object.keys(delta.add_tables).length &&
|
|
629
|
-
!delta.drop_tables.length &&
|
|
630
|
-
!Object.keys(delta.alter_tables).length &&
|
|
631
|
-
!delta.add_indexes.length &&
|
|
632
|
-
!delta.drop_indexes.length);
|
|
633
|
-
}
|
|
634
|
-
const deltaColors = {
|
|
635
|
-
'+': 'green',
|
|
636
|
-
'-': 'red',
|
|
637
|
-
'*': 'white',
|
|
638
|
-
};
|
|
639
|
-
export function* displayDelta(delta) {
|
|
640
|
-
const tables = [
|
|
641
|
-
...Object.keys(delta.add_tables).map(name => ({ op: '+', name })),
|
|
642
|
-
...Object.entries(delta.alter_tables).map(([name, changes]) => ({ op: '*', name, changes })),
|
|
643
|
-
...delta.drop_tables.map(name => ({ op: '-', name })),
|
|
644
|
-
];
|
|
645
|
-
tables.sort((a, b) => a.name.localeCompare(b.name));
|
|
646
|
-
for (const table of tables) {
|
|
647
|
-
yield styleText(deltaColors[table.op], `${table.op} table ${table.name}`);
|
|
648
|
-
if (table.op != '*')
|
|
649
|
-
continue;
|
|
650
|
-
const columns = [
|
|
651
|
-
...Object.keys(table.changes.add_columns).map(name => ({ op: '+', name })),
|
|
652
|
-
...table.changes.drop_columns.map(name => ({ op: '-', name })),
|
|
653
|
-
...Object.entries(table.changes.alter_columns).map(([name, changes]) => ({ op: '*', name, ...changes })),
|
|
654
|
-
];
|
|
655
|
-
columns.sort((a, b) => a.name.localeCompare(b.name));
|
|
656
|
-
for (const column of columns) {
|
|
657
|
-
const columnChanges = column.op == '*'
|
|
658
|
-
? [...(column.ops ?? []), 'default' in column && 'set_default', 'type' in column && 'set_type']
|
|
659
|
-
.filter((e) => !!e)
|
|
660
|
-
.map(e => e.replaceAll('_', ' '))
|
|
661
|
-
.join(', ')
|
|
662
|
-
: null;
|
|
663
|
-
yield '\t' +
|
|
664
|
-
styleText(deltaColors[column.op], `${column.op} column ${column.name}${column.op != '*' ? '' : ': ' + columnChanges}`);
|
|
665
|
-
}
|
|
666
|
-
const constraints = [
|
|
667
|
-
...Object.keys(table.changes.add_constraints).map(name => ({ op: '+', name })),
|
|
668
|
-
...table.changes.drop_constraints.map(name => ({ op: '-', name })),
|
|
669
|
-
];
|
|
670
|
-
for (const con of constraints) {
|
|
671
|
-
yield '\t' + styleText(deltaColors[con.op], `${con.op} constraint ${con.name}`);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
const indexes = [
|
|
675
|
-
...delta.add_indexes.map(raw => ({ op: '+', ...parseIndex(raw) })),
|
|
676
|
-
...delta.drop_indexes.map(raw => ({ op: '-', ...parseIndex(raw) })),
|
|
677
|
-
];
|
|
678
|
-
indexes.sort((a, b) => a.table.localeCompare(b.table) || a.column.localeCompare(b.column));
|
|
679
|
-
for (const index of indexes) {
|
|
680
|
-
yield styleText(deltaColors[index.op], `${index.op} index on ${index.table}.${index.column}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
function columnFromSchema(column, allowPK) {
|
|
684
|
-
return function _addColumn(col) {
|
|
685
|
-
if (column.primary && allowPK)
|
|
686
|
-
col = col.primaryKey();
|
|
687
|
-
if (column.unique)
|
|
688
|
-
col = col.unique();
|
|
689
|
-
if (column.required)
|
|
690
|
-
col = col.notNull();
|
|
691
|
-
else if (column.unique)
|
|
692
|
-
col = col.nullsNotDistinct();
|
|
693
|
-
if (column.references)
|
|
694
|
-
col = col.references(column.references);
|
|
695
|
-
if (column.onDelete)
|
|
696
|
-
col = col.onDelete(column.onDelete);
|
|
697
|
-
if ('default' in column)
|
|
698
|
-
col = col.defaultTo(sql.raw(String(column.default)));
|
|
699
|
-
if (column.check)
|
|
700
|
-
col = col.check(sql.raw(column.check));
|
|
701
|
-
return col;
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
export async function applyDelta(delta, forceAbort = false) {
|
|
705
|
-
const tx = await database.startTransaction().execute();
|
|
706
|
-
try {
|
|
707
|
-
for (const [tableName, table] of Object.entries(delta.add_tables)) {
|
|
708
|
-
io.start('Adding table ' + tableName);
|
|
709
|
-
let query = tx.schema.createTable(tableName);
|
|
710
|
-
const columns = Object.entries(table.columns);
|
|
711
|
-
const pkColumns = columns.filter(([, column]) => column.primary).map(([name, column]) => ({ name, ...column }));
|
|
712
|
-
const needsSpecialConstraint = pkColumns.length > 1 || pkColumns.some(col => !col.required);
|
|
713
|
-
for (const [colName, column] of columns) {
|
|
714
|
-
query = query.addColumn(colName, sql.raw(column.type), columnFromSchema(column, !needsSpecialConstraint));
|
|
715
|
-
}
|
|
716
|
-
if (needsSpecialConstraint) {
|
|
717
|
-
query = query.addPrimaryKeyConstraint('PK_' + tableName.replaceAll('.', '_'), pkColumns.map(col => col.name));
|
|
718
|
-
}
|
|
719
|
-
await query.execute();
|
|
720
|
-
io.done();
|
|
721
|
-
}
|
|
722
|
-
for (const tableName of delta.drop_tables) {
|
|
723
|
-
io.start('Dropping table ' + tableName);
|
|
724
|
-
await tx.schema.dropTable(tableName).execute();
|
|
725
|
-
io.done();
|
|
726
|
-
}
|
|
727
|
-
for (const [tableName, tableDelta] of Object.entries(delta.alter_tables)) {
|
|
728
|
-
io.start(`Modifying table ${tableName}`);
|
|
729
|
-
const query = tx.schema.alterTable(tableName);
|
|
730
|
-
for (const constraint of tableDelta.drop_constraints) {
|
|
731
|
-
await query.dropConstraint(constraint).execute();
|
|
732
|
-
}
|
|
733
|
-
for (const colName of tableDelta.drop_columns) {
|
|
734
|
-
await query.dropColumn(colName).execute();
|
|
735
|
-
}
|
|
736
|
-
for (const [colName, column] of Object.entries(tableDelta.add_columns)) {
|
|
737
|
-
await query.addColumn(colName, sql.raw(column.type), columnFromSchema(column, false)).execute();
|
|
738
|
-
}
|
|
739
|
-
for (const [colName, column] of Object.entries(tableDelta.alter_columns)) {
|
|
740
|
-
if (column.default)
|
|
741
|
-
await query.alterColumn(colName, col => col.setDefault(sql.raw(String(column.default)))).execute();
|
|
742
|
-
if (column.type)
|
|
743
|
-
await query.alterColumn(colName, col => col.setDataType(sql.raw(column.type))).execute();
|
|
744
|
-
for (const op of column.ops ?? []) {
|
|
745
|
-
switch (op) {
|
|
746
|
-
case 'drop_default':
|
|
747
|
-
if (column.default)
|
|
748
|
-
throw 'Cannot set and drop default at the same time';
|
|
749
|
-
await query.alterColumn(colName, col => col.dropDefault()).execute();
|
|
750
|
-
break;
|
|
751
|
-
case 'set_required':
|
|
752
|
-
await query.alterColumn(colName, col => col.setNotNull()).execute();
|
|
753
|
-
break;
|
|
754
|
-
case 'drop_required':
|
|
755
|
-
await query.alterColumn(colName, col => col.dropNotNull()).execute();
|
|
756
|
-
break;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
for (const [name, con] of Object.entries(tableDelta.add_constraints)) {
|
|
761
|
-
switch (con.type) {
|
|
762
|
-
case 'unique':
|
|
763
|
-
await query.addUniqueConstraint(name, con.on, b => (con.nulls_not_distinct ? b.nullsNotDistinct() : b)).execute();
|
|
764
|
-
break;
|
|
765
|
-
case 'check':
|
|
766
|
-
await query.addCheckConstraint(name, sql.raw(con.check)).execute();
|
|
767
|
-
break;
|
|
768
|
-
case 'foreign_key':
|
|
769
|
-
await query.addForeignKeyConstraint(name, con.on, con.target, con.references, b => b).execute();
|
|
770
|
-
break;
|
|
771
|
-
case 'primary_key':
|
|
772
|
-
await query.addPrimaryKeyConstraint(name, con.on).execute();
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
io.done();
|
|
777
|
-
}
|
|
778
|
-
if (forceAbort)
|
|
779
|
-
throw 'Rolling back due to --abort';
|
|
780
|
-
io.start('Committing');
|
|
781
|
-
await tx.commit().execute();
|
|
782
|
-
io.done();
|
|
783
|
-
}
|
|
784
|
-
catch (e) {
|
|
785
|
-
await tx.rollback().execute();
|
|
786
|
-
if (e instanceof SuppressedError)
|
|
787
|
-
io.error(e.suppressed);
|
|
788
|
-
throw e;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
242
|
/**
|
|
792
243
|
* Checks that a table has the expected column types, nullability, and default values.
|
|
793
244
|
*/
|
|
@@ -802,7 +253,7 @@ export async function checkTableTypes(tableName, types, opt, tableMetadata) {
|
|
|
802
253
|
for (const [i, [key, { type, required = false, default: _default }]] of _types.entries()) {
|
|
803
254
|
io.progress(i, _types.length, key);
|
|
804
255
|
const col = columns[key];
|
|
805
|
-
const actualType = type in
|
|
256
|
+
const actualType = type in schema.toIntrospected ? schema.toIntrospected[type] : type;
|
|
806
257
|
const hasDefault = _default !== undefined;
|
|
807
258
|
try {
|
|
808
259
|
if (!col)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as io from '@axium/core/node/io';
|
|
2
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
import config from '../config.js';
|
|
5
|
+
const sym = Symbol.for('Axium:database');
|
|
6
|
+
export let database;
|
|
7
|
+
export function connect() {
|
|
8
|
+
if (database)
|
|
9
|
+
return database;
|
|
10
|
+
if (globalThis[sym])
|
|
11
|
+
return (database = globalThis[sym]);
|
|
12
|
+
database = new Kysely({
|
|
13
|
+
dialect: new PostgresDialect({ pool: new pg.Pool(config.db) }),
|
|
14
|
+
});
|
|
15
|
+
globalThis[sym] = database;
|
|
16
|
+
io.debug('Connected to database!');
|
|
17
|
+
return database;
|
|
18
|
+
}
|