@axium/server 0.26.3 → 0.28.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.d.ts +26 -45
- package/dist/acl.js +50 -52
- package/dist/api/acl.js +16 -8
- package/dist/api/admin.js +9 -12
- package/dist/api/metadata.js +4 -11
- package/dist/api/passkeys.js +6 -6
- package/dist/api/register.js +1 -1
- package/dist/api/users.js +16 -30
- package/dist/audit.d.ts +3 -3
- package/dist/audit.js +8 -9
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +29 -23
- package/dist/cli.d.ts +8 -2
- package/dist/cli.js +18 -605
- package/dist/config.d.ts +2 -2
- package/dist/config.js +8 -7
- package/dist/database.d.ts +417 -29
- package/dist/database.js +546 -247
- package/dist/db.json +71 -0
- package/dist/internal_requests.js +1 -1
- package/dist/main.d.ts +2 -0
- package/dist/main.js +833 -0
- package/dist/requests.d.ts +1 -1
- package/dist/requests.js +8 -1
- package/dist/routes.d.ts +20 -20
- package/dist/routes.js +2 -1
- package/dist/serve.js +1 -1
- package/package.json +6 -4
- package/routes/account/+page.svelte +11 -13
- package/routes/admin/audit/[id]/+page.svelte +6 -1
- package/routes/admin/plugins/+page.svelte +5 -1
- package/schemas/config.json +207 -0
- package/schemas/db.json +636 -0
- package/svelte.config.js +3 -0
package/dist/database.js
CHANGED
|
@@ -55,10 +55,14 @@ import { plugins } from '@axium/core/plugins';
|
|
|
55
55
|
import { Kysely, PostgresDialect, sql } from 'kysely';
|
|
56
56
|
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
57
57
|
import { randomBytes } from 'node:crypto';
|
|
58
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
58
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
59
|
+
import { join } from 'node:path/posix';
|
|
60
|
+
import { styleText } from 'node:util';
|
|
59
61
|
import pg from 'pg';
|
|
62
|
+
import * as z from 'zod';
|
|
60
63
|
import config from './config.js';
|
|
61
|
-
import
|
|
64
|
+
import rawSchema from './db.json' with { type: 'json' };
|
|
65
|
+
import { dirs, systemDir } from './io.js';
|
|
62
66
|
const sym = Symbol.for('Axium:database');
|
|
63
67
|
export let database;
|
|
64
68
|
export function connect() {
|
|
@@ -93,6 +97,30 @@ export function userFromId(builder) {
|
|
|
93
97
|
.$castTo()
|
|
94
98
|
.as('user');
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Used for `update ... set ... from`
|
|
102
|
+
*/
|
|
103
|
+
export function values(records, alias) {
|
|
104
|
+
if (!records?.length)
|
|
105
|
+
throw new Error('Can not create values() with empty records array');
|
|
106
|
+
// Assume there's at least one record and all records
|
|
107
|
+
// have the same keys.
|
|
108
|
+
const keys = Object.keys(records[0]);
|
|
109
|
+
// Transform the records into a list of lists such as
|
|
110
|
+
// ($1, $2, $3), ($4, $5, $6)
|
|
111
|
+
const values = sql.join(records.map(r => sql `(${sql.join(keys.map(k => r[k]))})`));
|
|
112
|
+
// Create the alias `v(id, v1, v2)` that specifies the table alias
|
|
113
|
+
// AND a name for each column.
|
|
114
|
+
const wrappedAlias = sql.ref(alias);
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
116
|
+
const wrappedColumns = sql.join(keys.map(sql.ref));
|
|
117
|
+
const aliasSql = sql `${wrappedAlias}(${wrappedColumns})`;
|
|
118
|
+
// Finally create a single `AliasedRawBuilder` instance of the
|
|
119
|
+
// whole thing. Note that we need to explicitly specify
|
|
120
|
+
// the alias type using `.as<A>` because we are using a
|
|
121
|
+
// raw sql snippet as the alias.
|
|
122
|
+
return sql `(values ${values})`.as(aliasSql);
|
|
123
|
+
}
|
|
96
124
|
export async function statText() {
|
|
97
125
|
try {
|
|
98
126
|
const stats = await count('users', 'passkeys', 'sessions');
|
|
@@ -133,26 +161,16 @@ export async function getHBA(opt) {
|
|
|
133
161
|
};
|
|
134
162
|
return [content, writeBack];
|
|
135
163
|
}
|
|
136
|
-
|
|
164
|
+
/** @internal @hidden */
|
|
165
|
+
export const _pgHba = `
|
|
137
166
|
local axium axium md5
|
|
138
167
|
host axium axium 127.0.0.1/32 md5
|
|
139
168
|
host axium axium ::1/128 md5
|
|
140
169
|
`;
|
|
141
|
-
|
|
170
|
+
/** @internal @hidden */
|
|
171
|
+
export const _sql = (command, message) => io.run(message, `sudo -u postgres psql -c "${command}"`);
|
|
142
172
|
/** Shortcut to output a warning if an error is thrown because relation already exists */
|
|
143
173
|
export const warnExists = io.someWarnings([/\w+ "[\w.]+" already exists/, 'already exists.']);
|
|
144
|
-
const throwUnlessRows = (text) => {
|
|
145
|
-
if (text.includes('(0 rows)'))
|
|
146
|
-
throw 'missing.';
|
|
147
|
-
return text;
|
|
148
|
-
};
|
|
149
|
-
export async function createIndex(table, column, mod) {
|
|
150
|
-
io.start(`Creating index for ${table}.${column}`);
|
|
151
|
-
let query = database.schema.createIndex(`${table}_${column}_index`).on(table).column(column);
|
|
152
|
-
if (mod)
|
|
153
|
-
query = mod(query);
|
|
154
|
-
await query.execute().then(io.done).catch(warnExists);
|
|
155
|
-
}
|
|
156
174
|
export async function init(opt) {
|
|
157
175
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
158
176
|
try {
|
|
@@ -186,11 +204,11 @@ export async function init(opt) {
|
|
|
186
204
|
await getHBA(opt)
|
|
187
205
|
.then(([content, writeBack]) => {
|
|
188
206
|
io.start('Checking for Axium HBA configuration');
|
|
189
|
-
if (content.includes(
|
|
207
|
+
if (content.includes(_pgHba))
|
|
190
208
|
throw 'already exists.';
|
|
191
209
|
io.done();
|
|
192
210
|
io.start('Adding Axium HBA configuration');
|
|
193
|
-
const newContent = content.replace(/^local\s+all\s+all.*$/m, `$&\n${
|
|
211
|
+
const newContent = content.replace(/^local\s+all\s+all.*$/m, `$&\n${_pgHba}`);
|
|
194
212
|
io.done();
|
|
195
213
|
writeBack(newContent);
|
|
196
214
|
})
|
|
@@ -199,91 +217,8 @@ export async function init(opt) {
|
|
|
199
217
|
io.start('Connecting to database');
|
|
200
218
|
const _ = __addDisposableResource(env_1, connect(), true);
|
|
201
219
|
io.done();
|
|
202
|
-
function maybeCheck(table) {
|
|
203
|
-
return (e) => {
|
|
204
|
-
warnExists(e);
|
|
205
|
-
if (opt.check)
|
|
206
|
-
return checkTableTypes(table, expectedTypes[table], opt);
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
io.start('Creating table users');
|
|
210
|
-
await database.schema
|
|
211
|
-
.createTable('users')
|
|
212
|
-
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
213
|
-
.addColumn('name', 'text')
|
|
214
|
-
.addColumn('email', 'text', col => col.unique().notNull())
|
|
215
|
-
.addColumn('emailVerified', 'timestamptz')
|
|
216
|
-
.addColumn('image', 'text')
|
|
217
|
-
.addColumn('isAdmin', 'boolean', col => col.notNull().defaultTo(false))
|
|
218
|
-
.addColumn('roles', sql `text[]`, col => col.notNull().defaultTo(sql `'{}'::text[]`))
|
|
219
|
-
.addColumn('tags', sql `text[]`, col => col.notNull().defaultTo(sql `'{}'::text[]`))
|
|
220
|
-
.addColumn('preferences', 'jsonb', col => col.notNull().defaultTo('{}'))
|
|
221
|
-
.addColumn('registeredAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
|
|
222
|
-
.addColumn('isSuspended', 'boolean', col => col.notNull().defaultTo(false))
|
|
223
|
-
.execute()
|
|
224
|
-
.then(io.done)
|
|
225
|
-
.catch(maybeCheck('users'));
|
|
226
|
-
io.start('Creating table sessions');
|
|
227
|
-
await database.schema
|
|
228
|
-
.createTable('sessions')
|
|
229
|
-
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
230
|
-
.addColumn('userId', 'uuid', col => col.references('users.id').onDelete('cascade').notNull())
|
|
231
|
-
.addColumn('token', 'text', col => col.notNull().unique())
|
|
232
|
-
.addColumn('created', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
|
|
233
|
-
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
234
|
-
.addColumn('elevated', 'boolean', col => col.notNull())
|
|
235
|
-
.execute()
|
|
236
|
-
.then(io.done)
|
|
237
|
-
.catch(maybeCheck('sessions'));
|
|
238
|
-
await createIndex('sessions', 'id');
|
|
239
|
-
io.start('Creating table verifications');
|
|
240
|
-
await database.schema
|
|
241
|
-
.createTable('verifications')
|
|
242
|
-
.addColumn('userId', 'uuid', col => col.references('users.id').onDelete('cascade').notNull())
|
|
243
|
-
.addColumn('token', 'text', col => col.notNull().unique())
|
|
244
|
-
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
245
|
-
.addColumn('role', 'text', col => col.notNull())
|
|
246
|
-
.execute()
|
|
247
|
-
.then(io.done)
|
|
248
|
-
.catch(maybeCheck('verifications'));
|
|
249
|
-
io.start('Creating table passkeys');
|
|
250
|
-
await database.schema
|
|
251
|
-
.createTable('passkeys')
|
|
252
|
-
.addColumn('id', 'text', col => col.primaryKey().notNull())
|
|
253
|
-
.addColumn('name', 'text')
|
|
254
|
-
.addColumn('createdAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
|
|
255
|
-
.addColumn('userId', 'uuid', col => col.notNull().references('users.id').onDelete('cascade').notNull())
|
|
256
|
-
.addColumn('publicKey', 'bytea', col => col.notNull())
|
|
257
|
-
.addColumn('counter', 'integer', col => col.notNull())
|
|
258
|
-
.addColumn('deviceType', 'text', col => col.notNull())
|
|
259
|
-
.addColumn('backedUp', 'boolean', col => col.notNull())
|
|
260
|
-
.addColumn('transports', sql `text[]`)
|
|
261
|
-
.execute()
|
|
262
|
-
.then(io.done)
|
|
263
|
-
.catch(maybeCheck('passkeys'));
|
|
264
|
-
await createIndex('passkeys', 'userId');
|
|
265
|
-
io.start('Creating table audit_log');
|
|
266
|
-
await database.schema
|
|
267
|
-
.createTable('audit_log')
|
|
268
|
-
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
269
|
-
.addColumn('timestamp', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
|
|
270
|
-
.addColumn('userId', 'uuid')
|
|
271
|
-
.addColumn('severity', 'integer', col => col.notNull())
|
|
272
|
-
.addColumn('name', 'text', col => col.notNull())
|
|
273
|
-
.addColumn('source', 'text', col => col.notNull())
|
|
274
|
-
.addColumn('tags', sql `text[]`, col => col.notNull().defaultTo(sql `'{}'::text[]`))
|
|
275
|
-
.addColumn('extra', 'jsonb', col => col.notNull().defaultTo('{}'))
|
|
276
|
-
.execute()
|
|
277
|
-
.then(io.done)
|
|
278
|
-
.catch(maybeCheck('audit_log'));
|
|
279
220
|
io.start('Creating schema acl');
|
|
280
221
|
await database.schema.createSchema('acl').execute().then(io.done).catch(warnExists);
|
|
281
|
-
for (const plugin of plugins.values()) {
|
|
282
|
-
if (!plugin._hooks?.db_init)
|
|
283
|
-
continue;
|
|
284
|
-
io.log(styleText('whiteBright', 'Running plugin: '), plugin.name);
|
|
285
|
-
await plugin._hooks?.db_init(opt);
|
|
286
|
-
}
|
|
287
222
|
}
|
|
288
223
|
catch (e_1) {
|
|
289
224
|
env_1.error = e_1;
|
|
@@ -295,85 +230,535 @@ export async function init(opt) {
|
|
|
295
230
|
await result_1;
|
|
296
231
|
}
|
|
297
232
|
}
|
|
298
|
-
export const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
233
|
+
export const Column = z.strictObject({
|
|
234
|
+
type: z.string(),
|
|
235
|
+
required: z.boolean().default(false),
|
|
236
|
+
unique: z.boolean().default(false),
|
|
237
|
+
primary: z.boolean().default(false),
|
|
238
|
+
references: z.string().optional(),
|
|
239
|
+
onDelete: z.enum(['cascade', 'restrict', 'no action', 'set null', 'set default']).optional(),
|
|
240
|
+
default: z.any().optional(),
|
|
241
|
+
check: z.string().optional(),
|
|
242
|
+
});
|
|
243
|
+
export const Constraint = z.discriminatedUnion('type', [
|
|
244
|
+
z.strictObject({
|
|
245
|
+
type: z.literal('primary_key'),
|
|
246
|
+
on: z.string().array(),
|
|
247
|
+
}),
|
|
248
|
+
z.strictObject({
|
|
249
|
+
type: z.literal('foreign_key'),
|
|
250
|
+
on: z.string().array(),
|
|
251
|
+
target: z.string(),
|
|
252
|
+
references: z.string().array(),
|
|
253
|
+
}),
|
|
254
|
+
z.strictObject({
|
|
255
|
+
type: z.literal('unique'),
|
|
256
|
+
on: z.string().array(),
|
|
257
|
+
nulls_not_distinct: z.boolean().optional(),
|
|
258
|
+
}),
|
|
259
|
+
z.strictObject({
|
|
260
|
+
type: z.literal('check'),
|
|
261
|
+
check: z.string(),
|
|
262
|
+
}),
|
|
263
|
+
]);
|
|
264
|
+
export const Table = z.strictObject({
|
|
265
|
+
columns: z.record(z.string(), Column),
|
|
266
|
+
constraints: z.record(z.string(), Constraint).optional().default({}),
|
|
267
|
+
});
|
|
268
|
+
export const IndexString = z.templateLiteral([z.string(), ':', z.string()]);
|
|
269
|
+
export function parseIndex(value) {
|
|
270
|
+
const [table, column] = value.split(':');
|
|
271
|
+
return { table, column };
|
|
272
|
+
}
|
|
273
|
+
export const SchemaDecl = z.strictObject({
|
|
274
|
+
tables: z.record(z.string(), Table),
|
|
275
|
+
indexes: IndexString.array().optional().default([]),
|
|
276
|
+
});
|
|
277
|
+
export const ColumnDelta = z.strictObject({
|
|
278
|
+
type: z.string().optional(),
|
|
279
|
+
default: z.string().optional(),
|
|
280
|
+
ops: z.literal(['drop_default', 'set_required', 'drop_required']).array().optional(),
|
|
281
|
+
});
|
|
282
|
+
export const TableDelta = z.strictObject({
|
|
283
|
+
add_columns: z.record(z.string(), Column).optional().default({}),
|
|
284
|
+
drop_columns: z.string().array().optional().default([]),
|
|
285
|
+
alter_columns: z.record(z.string(), ColumnDelta).optional().default({}),
|
|
286
|
+
add_constraints: z.record(z.string(), Constraint).optional().default({}),
|
|
287
|
+
drop_constraints: z.string().array().optional().default([]),
|
|
288
|
+
});
|
|
289
|
+
export const VersionDelta = z.strictObject({
|
|
290
|
+
delta: z.literal(true),
|
|
291
|
+
add_tables: z.record(z.string(), Table).optional().default({}),
|
|
292
|
+
drop_tables: z.string().array().optional().default([]),
|
|
293
|
+
alter_tables: z.record(z.string(), TableDelta).optional().default({}),
|
|
294
|
+
add_indexes: IndexString.array().optional().default([]),
|
|
295
|
+
drop_indexes: IndexString.array().optional().default([]),
|
|
296
|
+
});
|
|
297
|
+
export const SchemaFile = z.object({
|
|
298
|
+
format: z.literal(0),
|
|
299
|
+
versions: z.discriminatedUnion('delta', [SchemaDecl.extend({ delta: z.literal(false) }), VersionDelta]).array(),
|
|
300
|
+
/** List of tables to wipe */
|
|
301
|
+
wipe: z.string().array().optional().default([]),
|
|
302
|
+
/** Set the latest version, defaults to the last one */
|
|
303
|
+
latest: z.number().nonnegative().optional(),
|
|
304
|
+
/** Maps tables to their ACL tables, e.g. `"storage": "acl.storage"` */
|
|
305
|
+
acl_tables: z.record(z.string(), z.string()).optional().default({}),
|
|
306
|
+
});
|
|
307
|
+
const { data, error } = SchemaFile.safeParse(rawSchema);
|
|
308
|
+
if (error)
|
|
309
|
+
io.error('Invalid base database schema:\n' + z.prettifyError(error));
|
|
310
|
+
const schema = data;
|
|
311
|
+
export function* getSchemaFiles() {
|
|
312
|
+
yield ['@axium/server', schema];
|
|
313
|
+
for (const [name, plugin] of plugins) {
|
|
314
|
+
if (!plugin._db)
|
|
315
|
+
continue;
|
|
316
|
+
try {
|
|
317
|
+
yield [name, SchemaFile.parse(plugin._db)];
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
const text = e instanceof z.core.$ZodError ? z.prettifyError(e) : e instanceof Error ? e.message : String(e);
|
|
321
|
+
throw `Invalid database configuration for plugin "${name}":\n${text}`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get the active schema
|
|
327
|
+
*/
|
|
328
|
+
export function getFullSchema(opt = {}) {
|
|
329
|
+
const fullSchema = { tables: {}, indexes: [] };
|
|
330
|
+
for (const [pluginName, file] of getSchemaFiles()) {
|
|
331
|
+
if (opt.exclude?.includes(pluginName))
|
|
332
|
+
continue;
|
|
333
|
+
let currentSchema = { tables: {}, indexes: [] };
|
|
334
|
+
for (const [version, schema] of file.versions.entries()) {
|
|
335
|
+
if (schema.delta)
|
|
336
|
+
applyDeltaToSchema(currentSchema, schema);
|
|
337
|
+
else
|
|
338
|
+
currentSchema = schema;
|
|
339
|
+
if (version === file.latest)
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
for (const name of Object.keys(currentSchema.tables)) {
|
|
343
|
+
if (name in fullSchema.tables)
|
|
344
|
+
throw 'Duplicate table name in database schema: ' + name;
|
|
345
|
+
fullSchema.tables[name] = currentSchema.tables[name];
|
|
346
|
+
}
|
|
347
|
+
for (const index of currentSchema.indexes) {
|
|
348
|
+
if (fullSchema.indexes.includes(index))
|
|
349
|
+
throw 'Duplicate index in database schema: ' + index;
|
|
350
|
+
fullSchema.indexes.push(index);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return fullSchema;
|
|
354
|
+
}
|
|
355
|
+
const schemaToIntrospected = {
|
|
356
|
+
boolean: 'bool',
|
|
357
|
+
integer: 'int4',
|
|
358
|
+
'text[]': '_text',
|
|
359
|
+
};
|
|
360
|
+
const VersionMap = z.record(z.string(), z.int().nonnegative());
|
|
361
|
+
export const UpgradesInfo = z.object({
|
|
362
|
+
current: VersionMap.default({}),
|
|
363
|
+
upgrades: z.object({ timestamp: z.coerce.date(), from: VersionMap, to: VersionMap }).array().default([]),
|
|
364
|
+
});
|
|
365
|
+
const upgradesFilePath = process.getuid?.() == 0 ? join(systemDir, 'db_upgrades.json') : join(dirs.at(-1), 'db_upgrades.json');
|
|
366
|
+
export function getUpgradeInfo() {
|
|
367
|
+
if (!existsSync(upgradesFilePath))
|
|
368
|
+
io.writeJSON(upgradesFilePath, { current: {}, upgrades: [] });
|
|
369
|
+
return io.readJSON(upgradesFilePath, UpgradesInfo);
|
|
370
|
+
}
|
|
371
|
+
export function setUpgradeInfo(info) {
|
|
372
|
+
io.writeJSON(upgradesFilePath, info);
|
|
373
|
+
}
|
|
374
|
+
export function applyTableDeltaToSchema(table, delta) {
|
|
375
|
+
for (const column of delta.drop_columns) {
|
|
376
|
+
if (column in table)
|
|
377
|
+
delete table.columns[column];
|
|
378
|
+
else
|
|
379
|
+
throw `Can't drop column ${column} because it does not exist`;
|
|
380
|
+
}
|
|
381
|
+
for (const [name, column] of Object.entries(delta.add_columns)) {
|
|
382
|
+
if (name in table)
|
|
383
|
+
throw `Can't add column ${name} because it already exists`;
|
|
384
|
+
table.columns[name] = column;
|
|
385
|
+
}
|
|
386
|
+
for (const [name, columnDelta] of Object.entries(delta.alter_columns)) {
|
|
387
|
+
const column = table.columns[name];
|
|
388
|
+
if (!column)
|
|
389
|
+
throw `Can't modify column ${name} because it does not exist`;
|
|
390
|
+
if (columnDelta.type)
|
|
391
|
+
column.type = columnDelta.type;
|
|
392
|
+
if (columnDelta.default)
|
|
393
|
+
column.default = columnDelta.default;
|
|
394
|
+
for (const op of columnDelta.ops || []) {
|
|
395
|
+
switch (op) {
|
|
396
|
+
case 'drop_default':
|
|
397
|
+
delete column.default;
|
|
398
|
+
break;
|
|
399
|
+
case 'set_required':
|
|
400
|
+
column.required = true;
|
|
401
|
+
break;
|
|
402
|
+
case 'drop_required':
|
|
403
|
+
column.required = false;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const name of delta.drop_constraints) {
|
|
409
|
+
if (table.constraints[name])
|
|
410
|
+
delete table.constraints[name];
|
|
411
|
+
else
|
|
412
|
+
throw `Can't drop constraint ${name} because it does not exist`;
|
|
413
|
+
}
|
|
414
|
+
for (const [name, constraint] of Object.entries(delta.add_constraints)) {
|
|
415
|
+
if (table.constraints[name])
|
|
416
|
+
throw `Can't add constraint ${name} because it already exists`;
|
|
417
|
+
table.constraints[name] = constraint;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
export function applyDeltaToSchema(schema, delta) {
|
|
421
|
+
for (const tableName of delta.drop_tables) {
|
|
422
|
+
if (tableName in schema.tables)
|
|
423
|
+
delete schema.tables[tableName];
|
|
424
|
+
else
|
|
425
|
+
throw `Can't drop table ${tableName} because it does not exist`;
|
|
426
|
+
}
|
|
427
|
+
for (const [tableName, table] of Object.entries(delta.add_tables)) {
|
|
428
|
+
if (tableName in schema.tables)
|
|
429
|
+
throw `Can't add table ${tableName} because it already exists`;
|
|
430
|
+
else
|
|
431
|
+
schema.tables[tableName] = table;
|
|
432
|
+
}
|
|
433
|
+
for (const [tableName, tableDelta] of Object.entries(delta.alter_tables)) {
|
|
434
|
+
if (tableName in schema.tables)
|
|
435
|
+
applyTableDeltaToSchema(schema.tables[tableName], tableDelta);
|
|
436
|
+
else
|
|
437
|
+
throw `Can't modify table ${tableName} because it does not exist`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
export function validateDelta(delta) {
|
|
441
|
+
const tableNames = [...Object.keys(delta.add_tables), ...Object.keys(delta.alter_tables), delta.drop_tables];
|
|
442
|
+
const uniqueTables = new Set(tableNames);
|
|
443
|
+
for (const table of uniqueTables) {
|
|
444
|
+
tableNames.splice(tableNames.indexOf(table), 1);
|
|
445
|
+
}
|
|
446
|
+
if (tableNames.length) {
|
|
447
|
+
throw `Duplicate table name(s): ${tableNames.join(', ')}`;
|
|
448
|
+
}
|
|
449
|
+
for (const [tableName, table] of Object.entries(delta.alter_tables)) {
|
|
450
|
+
const columnNames = [...Object.keys(table.add_columns), ...table.drop_columns];
|
|
451
|
+
const uniqueColumns = new Set(columnNames);
|
|
452
|
+
for (const column of uniqueColumns) {
|
|
453
|
+
columnNames.splice(columnNames.indexOf(column), 1);
|
|
454
|
+
}
|
|
455
|
+
if (columnNames.length) {
|
|
456
|
+
throw `Duplicate column name(s) in table ${tableName}: ${columnNames.join(', ')}`;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
export function computeDelta(from, to) {
|
|
461
|
+
const fromTables = new Set(Object.keys(from.tables));
|
|
462
|
+
const toTables = new Set(Object.keys(to.tables));
|
|
463
|
+
const fromIndexes = new Set(from.indexes);
|
|
464
|
+
const toIndexes = new Set(to.indexes);
|
|
465
|
+
const add_tables = Object.fromEntries(toTables
|
|
466
|
+
.difference(fromTables)
|
|
467
|
+
.keys()
|
|
468
|
+
.map(name => [name, to.tables[name]]));
|
|
469
|
+
const alter_tables = {};
|
|
470
|
+
for (const name of fromTables.intersection(toTables)) {
|
|
471
|
+
const fromTable = from.tables[name], toTable = to.tables[name];
|
|
472
|
+
const fromColumns = new Set(Object.keys(fromTable));
|
|
473
|
+
const toColumns = new Set(Object.keys(toTable));
|
|
474
|
+
const drop_columns = fromColumns.difference(toColumns);
|
|
475
|
+
const add_columns = Object.fromEntries(toColumns
|
|
476
|
+
.difference(fromColumns)
|
|
477
|
+
.keys()
|
|
478
|
+
.map(colName => [colName, toTable.columns[colName]]));
|
|
479
|
+
const alter_columns = Object.fromEntries(toColumns
|
|
480
|
+
.intersection(fromColumns)
|
|
481
|
+
.keys()
|
|
482
|
+
.map(name => {
|
|
483
|
+
const fromCol = fromTable.columns[name], toCol = toTable.columns[name];
|
|
484
|
+
const alter = { ops: [] };
|
|
485
|
+
if ('default' in fromCol && !('default' in toCol))
|
|
486
|
+
alter.ops.push('drop_default');
|
|
487
|
+
else if (fromCol.default !== toCol.default)
|
|
488
|
+
alter.default = toCol.default;
|
|
489
|
+
if (fromCol.type != toCol.type)
|
|
490
|
+
alter.type = toCol.type;
|
|
491
|
+
if (fromCol.required != toCol.required)
|
|
492
|
+
alter.ops.push(toCol.required ? 'set_required' : 'drop_required');
|
|
493
|
+
return [name, alter];
|
|
494
|
+
}));
|
|
495
|
+
const fromConstraints = new Set(Object.keys(fromTable.constraints || {}));
|
|
496
|
+
const toConstraints = new Set(Object.keys(toTable.constraints || {}));
|
|
497
|
+
const drop_constraints = fromConstraints.difference(toConstraints);
|
|
498
|
+
const add_constraints = Object.fromEntries(toConstraints
|
|
499
|
+
.difference(fromConstraints)
|
|
500
|
+
.keys()
|
|
501
|
+
.map(constName => [constName, toTable.constraints[constName]]));
|
|
502
|
+
alter_tables[name] = {
|
|
503
|
+
add_columns,
|
|
504
|
+
drop_columns: Array.from(drop_columns),
|
|
505
|
+
alter_columns,
|
|
506
|
+
add_constraints,
|
|
507
|
+
drop_constraints: Array.from(drop_constraints),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
delta: true,
|
|
512
|
+
add_tables,
|
|
513
|
+
drop_tables: Array.from(fromTables.difference(toTables)),
|
|
514
|
+
alter_tables,
|
|
515
|
+
drop_indexes: Array.from(fromIndexes.difference(toIndexes)),
|
|
516
|
+
add_indexes: Array.from(toIndexes.difference(fromIndexes)),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
export function collapseDeltas(deltas) {
|
|
520
|
+
const add_tables = {}, drop_tables = [], alter_tables = {}, add_indexes = [], drop_indexes = [];
|
|
521
|
+
for (const delta of deltas) {
|
|
522
|
+
validateDelta(delta);
|
|
523
|
+
for (const [name, table] of Object.entries(delta.alter_tables)) {
|
|
524
|
+
if (name in add_tables) {
|
|
525
|
+
applyTableDeltaToSchema(add_tables[name], table);
|
|
526
|
+
}
|
|
527
|
+
else if (name in alter_tables) {
|
|
528
|
+
const existing = alter_tables[name];
|
|
529
|
+
for (const [colName, column] of Object.entries(table.add_columns)) {
|
|
530
|
+
existing.add_columns[colName] = column;
|
|
531
|
+
}
|
|
532
|
+
for (const colName of table.drop_columns) {
|
|
533
|
+
if (colName in existing.add_columns)
|
|
534
|
+
delete existing.add_columns[colName];
|
|
535
|
+
else
|
|
536
|
+
existing.drop_columns.push(colName);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else
|
|
540
|
+
alter_tables[name] = table;
|
|
541
|
+
}
|
|
542
|
+
for (const table of delta.drop_tables) {
|
|
543
|
+
if (table in add_tables)
|
|
544
|
+
delete add_tables[table];
|
|
545
|
+
else
|
|
546
|
+
drop_tables.push(table);
|
|
547
|
+
}
|
|
548
|
+
for (const [name, table] of Object.entries(delta.add_tables)) {
|
|
549
|
+
if (drop_tables.includes(name))
|
|
550
|
+
throw `Can't add and drop table "${name}" in the same change`;
|
|
551
|
+
if (name in alter_tables)
|
|
552
|
+
throw `Can't add and modify table "${name}" in the same change`;
|
|
553
|
+
add_tables[name] = table;
|
|
554
|
+
}
|
|
555
|
+
for (const index of delta.add_indexes) {
|
|
556
|
+
if (drop_indexes.includes(index))
|
|
557
|
+
throw `Can't add and drop index "${index}" in the same change`;
|
|
558
|
+
add_indexes.push(index);
|
|
559
|
+
}
|
|
560
|
+
for (const index of delta.drop_indexes) {
|
|
561
|
+
if (add_indexes.includes(index))
|
|
562
|
+
throw `Can't add and drop index "${index}" in the same change`;
|
|
563
|
+
drop_indexes.push(index);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return { delta: true, add_tables, drop_tables, alter_tables, add_indexes, drop_indexes };
|
|
567
|
+
}
|
|
568
|
+
export function deltaIsEmpty(delta) {
|
|
569
|
+
return (!Object.keys(delta.add_tables).length &&
|
|
570
|
+
!delta.drop_tables.length &&
|
|
571
|
+
!Object.keys(delta.alter_tables).length &&
|
|
572
|
+
!delta.add_indexes.length &&
|
|
573
|
+
!delta.drop_indexes.length);
|
|
574
|
+
}
|
|
575
|
+
const deltaColors = {
|
|
576
|
+
'+': 'green',
|
|
577
|
+
'-': 'red',
|
|
578
|
+
'*': 'white',
|
|
347
579
|
};
|
|
580
|
+
export function* displayDelta(delta) {
|
|
581
|
+
const tables = [
|
|
582
|
+
...Object.keys(delta.add_tables).map(name => ({ op: '+', name })),
|
|
583
|
+
...Object.entries(delta.alter_tables).map(([name, changes]) => ({ op: '*', name, changes })),
|
|
584
|
+
...delta.drop_tables.map(name => ({ op: '-', name })),
|
|
585
|
+
];
|
|
586
|
+
tables.sort((a, b) => a.name.localeCompare(b.name));
|
|
587
|
+
for (const table of tables) {
|
|
588
|
+
yield styleText(deltaColors[table.op], `${table.op} table ${table.name}`);
|
|
589
|
+
if (table.op != '*')
|
|
590
|
+
continue;
|
|
591
|
+
const columns = [
|
|
592
|
+
...Object.keys(table.changes.add_columns).map(name => ({ op: '+', name })),
|
|
593
|
+
...table.changes.drop_columns.map(name => ({ op: '-', name })),
|
|
594
|
+
...Object.entries(table.changes.alter_columns).map(([name, changes]) => ({ op: '*', name, ...changes })),
|
|
595
|
+
];
|
|
596
|
+
columns.sort((a, b) => a.name.localeCompare(b.name));
|
|
597
|
+
for (const column of columns) {
|
|
598
|
+
const columnChanges = column.op == '*'
|
|
599
|
+
? [...(column.ops ?? []), 'default' in column && 'set_default', 'type' in column && 'set_type']
|
|
600
|
+
.filter((e) => !!e)
|
|
601
|
+
.map(e => e.replaceAll('_', ' '))
|
|
602
|
+
.join(', ')
|
|
603
|
+
: null;
|
|
604
|
+
yield '\t' +
|
|
605
|
+
styleText(deltaColors[column.op], `${column.op} column ${column.name}${column.op != '*' ? '' : ': ' + columnChanges}`);
|
|
606
|
+
}
|
|
607
|
+
const constraints = [
|
|
608
|
+
...Object.keys(table.changes.add_constraints).map(name => ({ op: '+', name })),
|
|
609
|
+
...table.changes.drop_constraints.map(name => ({ op: '-', name })),
|
|
610
|
+
];
|
|
611
|
+
for (const con of constraints) {
|
|
612
|
+
yield '\t' + styleText(deltaColors[con.op], `${con.op} constraint ${con.name}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const indexes = [
|
|
616
|
+
...delta.add_indexes.map(raw => ({ op: '+', ...parseIndex(raw) })),
|
|
617
|
+
...delta.drop_indexes.map(raw => ({ op: '-', ...parseIndex(raw) })),
|
|
618
|
+
];
|
|
619
|
+
indexes.sort((a, b) => a.table.localeCompare(b.table) || a.column.localeCompare(b.column));
|
|
620
|
+
for (const index of indexes) {
|
|
621
|
+
yield styleText(deltaColors[index.op], `${index.op} index on ${index.table}.${index.column}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function columnFromSchema(column, allowPK) {
|
|
625
|
+
return function _addColumn(col) {
|
|
626
|
+
if (column.primary && allowPK)
|
|
627
|
+
col = col.primaryKey();
|
|
628
|
+
if (column.unique)
|
|
629
|
+
col = col.unique();
|
|
630
|
+
if (column.required)
|
|
631
|
+
col = col.notNull();
|
|
632
|
+
else if (column.unique)
|
|
633
|
+
col = col.nullsNotDistinct();
|
|
634
|
+
if (column.references)
|
|
635
|
+
col = col.references(column.references);
|
|
636
|
+
if (column.onDelete)
|
|
637
|
+
col = col.onDelete(column.onDelete);
|
|
638
|
+
if ('default' in column)
|
|
639
|
+
col = col.defaultTo(sql.raw(column.default));
|
|
640
|
+
if (column.check)
|
|
641
|
+
col = col.check(sql.raw(column.check));
|
|
642
|
+
return col;
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
export async function applyDelta(delta, forceAbort = false) {
|
|
646
|
+
const tx = await database.startTransaction().execute();
|
|
647
|
+
try {
|
|
648
|
+
for (const [tableName, table] of Object.entries(delta.add_tables)) {
|
|
649
|
+
io.start('Adding table ' + tableName);
|
|
650
|
+
let query = tx.schema.createTable(tableName);
|
|
651
|
+
const columns = Object.entries(table.columns);
|
|
652
|
+
const pkColumns = columns.filter(([, column]) => column.primary).map(([name, column]) => ({ name, ...column }));
|
|
653
|
+
const needsSpecialConstraint = pkColumns.length > 1 || pkColumns.some(col => !col.required);
|
|
654
|
+
for (const [colName, column] of columns) {
|
|
655
|
+
query = query.addColumn(colName, sql.raw(column.type), columnFromSchema(column, !needsSpecialConstraint));
|
|
656
|
+
}
|
|
657
|
+
if (needsSpecialConstraint) {
|
|
658
|
+
query = query.addPrimaryKeyConstraint('PK_' + tableName.replaceAll('.', '_'), pkColumns.map(col => col.name));
|
|
659
|
+
}
|
|
660
|
+
await query.execute();
|
|
661
|
+
io.done();
|
|
662
|
+
}
|
|
663
|
+
for (const tableName of delta.drop_tables) {
|
|
664
|
+
io.start('Dropping table ' + tableName);
|
|
665
|
+
await tx.schema.dropTable(tableName).execute();
|
|
666
|
+
io.done();
|
|
667
|
+
}
|
|
668
|
+
for (const [tableName, tableDelta] of Object.entries(delta.alter_tables)) {
|
|
669
|
+
io.start(`Modifying table ${tableName}`);
|
|
670
|
+
const query = tx.schema.alterTable(tableName);
|
|
671
|
+
for (const constraint of tableDelta.drop_constraints) {
|
|
672
|
+
await query.dropConstraint(constraint).execute();
|
|
673
|
+
}
|
|
674
|
+
for (const colName of tableDelta.drop_columns) {
|
|
675
|
+
await query.dropColumn(colName).execute();
|
|
676
|
+
}
|
|
677
|
+
for (const [colName, column] of Object.entries(tableDelta.add_columns)) {
|
|
678
|
+
await query.addColumn(colName, sql.raw(column.type), columnFromSchema(column, false)).execute();
|
|
679
|
+
}
|
|
680
|
+
for (const [colName, column] of Object.entries(tableDelta.alter_columns)) {
|
|
681
|
+
if (column.default)
|
|
682
|
+
await query.alterColumn(colName, col => col.setDefault(sql.raw(column.default))).execute();
|
|
683
|
+
if (column.type)
|
|
684
|
+
await query.alterColumn(colName, col => col.setDataType(sql.raw(column.type))).execute();
|
|
685
|
+
for (const op of column.ops ?? []) {
|
|
686
|
+
switch (op) {
|
|
687
|
+
case 'drop_default':
|
|
688
|
+
if (column.default)
|
|
689
|
+
throw 'Cannot set and drop default at the same time';
|
|
690
|
+
await query.alterColumn(colName, col => col.dropDefault()).execute();
|
|
691
|
+
break;
|
|
692
|
+
case 'set_required':
|
|
693
|
+
await query.alterColumn(colName, col => col.setNotNull()).execute();
|
|
694
|
+
break;
|
|
695
|
+
case 'drop_required':
|
|
696
|
+
await query.alterColumn(colName, col => col.dropNotNull()).execute();
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
for (const [name, con] of Object.entries(tableDelta.add_constraints)) {
|
|
702
|
+
switch (con.type) {
|
|
703
|
+
case 'unique':
|
|
704
|
+
await query.addUniqueConstraint(name, con.on, b => (con.nulls_not_distinct ? b.nullsNotDistinct() : b)).execute();
|
|
705
|
+
break;
|
|
706
|
+
case 'check':
|
|
707
|
+
await query.addCheckConstraint(name, sql.raw(con.check)).execute();
|
|
708
|
+
break;
|
|
709
|
+
case 'foreign_key':
|
|
710
|
+
await query.addForeignKeyConstraint(name, con.on, con.target, con.references, b => b).execute();
|
|
711
|
+
break;
|
|
712
|
+
case 'primary_key':
|
|
713
|
+
await query.addPrimaryKeyConstraint(name, con.on).execute();
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
io.done();
|
|
718
|
+
}
|
|
719
|
+
if (forceAbort)
|
|
720
|
+
throw 'Rolling back due to --abort';
|
|
721
|
+
io.start('Committing');
|
|
722
|
+
await tx.commit().execute();
|
|
723
|
+
io.done();
|
|
724
|
+
}
|
|
725
|
+
catch (e) {
|
|
726
|
+
await tx.rollback().execute();
|
|
727
|
+
if (e instanceof SuppressedError)
|
|
728
|
+
io.error(e.suppressed);
|
|
729
|
+
throw e;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
348
732
|
/**
|
|
349
733
|
* Checks that a table has the expected column types, nullability, and default values.
|
|
350
734
|
*/
|
|
351
735
|
export async function checkTableTypes(tableName, types, opt) {
|
|
352
|
-
io.start(`Checking
|
|
736
|
+
io.start(`Checking table ${tableName}`);
|
|
353
737
|
const dbTables = opt._metadata || (await database.introspection.getTables());
|
|
354
738
|
const table = dbTables.find(t => (t.schema == 'public' ? t.name : `${t.schema}.${t.name}`) === tableName);
|
|
355
739
|
if (!table)
|
|
356
740
|
throw 'missing.';
|
|
357
|
-
io.done();
|
|
358
741
|
const columns = Object.fromEntries(table.columns.map(c => [c.name, c]));
|
|
359
|
-
|
|
360
|
-
|
|
742
|
+
const _types = Object.entries(types.columns);
|
|
743
|
+
for (const [i, [key, { type, required = false, default: _default }]] of _types.entries()) {
|
|
744
|
+
io.progress(i, _types.length, key);
|
|
361
745
|
const col = columns[key];
|
|
362
|
-
|
|
363
|
-
|
|
746
|
+
const actualType = type in schemaToIntrospected ? schemaToIntrospected[type] : type;
|
|
747
|
+
const hasDefault = _default !== undefined;
|
|
364
748
|
try {
|
|
365
|
-
if (col
|
|
366
|
-
throw
|
|
749
|
+
if (!col)
|
|
750
|
+
throw 'missing.';
|
|
751
|
+
if (col.dataType != actualType)
|
|
752
|
+
throw `incorrect type "${col.dataType}", expected ${actualType} (${type})`;
|
|
367
753
|
if (col.isNullable != !required)
|
|
368
754
|
throw required ? 'nullable' : 'not nullable';
|
|
369
755
|
if (col.hasDefaultValue != hasDefault)
|
|
370
756
|
throw hasDefault ? 'missing default' : 'has default';
|
|
371
|
-
io.done();
|
|
372
757
|
}
|
|
373
758
|
catch (e) {
|
|
374
759
|
if (opt.strict)
|
|
375
|
-
throw e
|
|
376
|
-
io.warn(e);
|
|
760
|
+
throw `${tableName}.${key}: ${e}`;
|
|
761
|
+
io.warn(`${tableName}.${key}: ${e}`);
|
|
377
762
|
}
|
|
378
763
|
delete columns[key];
|
|
379
764
|
}
|
|
@@ -390,43 +775,6 @@ export async function checkTableTypes(tableName, types, opt) {
|
|
|
390
775
|
else
|
|
391
776
|
io.warn(unchecked);
|
|
392
777
|
}
|
|
393
|
-
export async function check(opt) {
|
|
394
|
-
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
395
|
-
try {
|
|
396
|
-
await io.run('Checking for sudo', 'which sudo');
|
|
397
|
-
await io.run('Checking for psql', 'which psql');
|
|
398
|
-
await _sql(`SELECT 1 FROM pg_database WHERE datname = 'axium'`, 'Checking for database').then(throwUnlessRows);
|
|
399
|
-
await _sql(`SELECT 1 FROM pg_roles WHERE rolname = 'axium'`, 'Checking for user').then(throwUnlessRows);
|
|
400
|
-
io.start('Connecting to database');
|
|
401
|
-
const _ = __addDisposableResource(env_2, connect(), true);
|
|
402
|
-
io.done();
|
|
403
|
-
io.start('Getting table metadata');
|
|
404
|
-
opt._metadata = await database.introspection.getTables();
|
|
405
|
-
const tables = Object.fromEntries(opt._metadata.map(t => [t.name, t]));
|
|
406
|
-
io.done();
|
|
407
|
-
for (const table of Object.keys(expectedTypes)) {
|
|
408
|
-
await checkTableTypes(table, expectedTypes[table], opt);
|
|
409
|
-
delete tables[table];
|
|
410
|
-
}
|
|
411
|
-
io.start('Checking for extra tables');
|
|
412
|
-
const unchecked = Object.keys(tables).join(', ');
|
|
413
|
-
if (!unchecked.length)
|
|
414
|
-
io.done();
|
|
415
|
-
else if (opt.strict)
|
|
416
|
-
throw unchecked;
|
|
417
|
-
else
|
|
418
|
-
io.warn(unchecked);
|
|
419
|
-
}
|
|
420
|
-
catch (e_2) {
|
|
421
|
-
env_2.error = e_2;
|
|
422
|
-
env_2.hasError = true;
|
|
423
|
-
}
|
|
424
|
-
finally {
|
|
425
|
-
const result_2 = __disposeResources(env_2);
|
|
426
|
-
if (result_2)
|
|
427
|
-
await result_2;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
778
|
export async function clean(opt) {
|
|
431
779
|
const now = new Date();
|
|
432
780
|
io.start('Removing expired sessions');
|
|
@@ -440,55 +788,6 @@ export async function clean(opt) {
|
|
|
440
788
|
await plugin._hooks?.clean(opt);
|
|
441
789
|
}
|
|
442
790
|
}
|
|
443
|
-
/**
|
|
444
|
-
* Completely remove Axium from the database.
|
|
445
|
-
*/
|
|
446
|
-
export async function uninstall(opt) {
|
|
447
|
-
for (const plugin of plugins.values()) {
|
|
448
|
-
if (!plugin._hooks?.remove)
|
|
449
|
-
continue;
|
|
450
|
-
io.log(styleText('whiteBright', 'Running plugin: '), plugin.name);
|
|
451
|
-
await plugin._hooks?.remove(opt);
|
|
452
|
-
}
|
|
453
|
-
await _sql('DROP DATABASE axium', 'Dropping database');
|
|
454
|
-
await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
|
|
455
|
-
await _sql('DROP USER axium', 'Dropping user');
|
|
456
|
-
await getHBA(opt)
|
|
457
|
-
.then(([content, writeBack]) => {
|
|
458
|
-
io.start('Checking for Axium HBA configuration');
|
|
459
|
-
if (!content.includes(pgHba))
|
|
460
|
-
throw 'missing.';
|
|
461
|
-
io.done();
|
|
462
|
-
io.start('Removing Axium HBA configuration');
|
|
463
|
-
const newContent = content.replace(pgHba, '');
|
|
464
|
-
io.done();
|
|
465
|
-
writeBack(newContent);
|
|
466
|
-
})
|
|
467
|
-
.catch(io.warn);
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Removes all data from tables.
|
|
471
|
-
*/
|
|
472
|
-
export async function wipe(opt) {
|
|
473
|
-
for (const plugin of plugins.values()) {
|
|
474
|
-
if (!plugin._hooks?.db_wipe)
|
|
475
|
-
continue;
|
|
476
|
-
io.log(styleText('whiteBright', 'Running plugin: '), plugin.name);
|
|
477
|
-
await plugin._hooks?.db_wipe(opt);
|
|
478
|
-
}
|
|
479
|
-
for (const table of ['users', 'passkeys', 'sessions', 'verifications']) {
|
|
480
|
-
io.start(`Wiping ${table}`);
|
|
481
|
-
await database.deleteFrom(table).execute();
|
|
482
|
-
io.done();
|
|
483
|
-
}
|
|
484
|
-
for (const table of await database.introspection.getTables()) {
|
|
485
|
-
if (!table.name.startsWith('acl.'))
|
|
486
|
-
continue;
|
|
487
|
-
const name = table.name;
|
|
488
|
-
io.debug(`Wiping ${name}`);
|
|
489
|
-
await database.deleteFrom(name).execute();
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
791
|
export async function rotatePassword() {
|
|
493
792
|
io.start('Generating new password');
|
|
494
793
|
const password = randomBytes(32).toString('base64');
|