build sqlite3 store
This commit is contained in:
parent
7addce60bb
commit
dc6e01db14
1224
package-lock.json
generated
1224
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,9 @@
|
|||||||
"url": "git@git.jbrumond.me:templates/nodejs-typescript-service.git"
|
"url": "git@git.jbrumond.me:templates/nodejs-typescript-service.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc --build"
|
"tsc": "tsc --build",
|
||||||
|
"clean:build": "rm -rf ./build/*",
|
||||||
|
"clean:data": "rm -rf ./data/*"
|
||||||
},
|
},
|
||||||
"author": "James Brumond <https://jbrumond.me>",
|
"author": "James Brumond <https://jbrumond.me>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -26,9 +28,11 @@
|
|||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"fast-xml-parser": "^4.2.6",
|
"fast-xml-parser": "^4.2.6",
|
||||||
"fastify": "^4.19.2",
|
"fastify": "^4.19.2",
|
||||||
|
"generic-pool": "^3.9.0",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
"openid-client": "^5.4.3",
|
"openid-client": "^5.4.3",
|
||||||
"pino": "^8.14.1",
|
"pino": "^8.14.1",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,8 @@
|
|||||||
"title": "Storage Config",
|
"title": "Storage Config",
|
||||||
"description": "Configuration for the main application data storage layer",
|
"description": "Configuration for the main application data storage layer",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{ "$ref": "#/$defs/file_storage_config" }
|
{ "$ref": "#/$defs/file_storage_config" },
|
||||||
|
{ "$ref": "#/$defs/sqlite3_storage_config" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -248,6 +249,30 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"engine"
|
"engine"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"sqlite3_storage_config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"engine": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sqlite3"
|
||||||
|
},
|
||||||
|
"pool_min": {
|
||||||
|
"description": "",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 2,
|
||||||
|
"maximum": 100
|
||||||
|
},
|
||||||
|
"pool_max": {
|
||||||
|
"description": "",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 2,
|
||||||
|
"maximum": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"engine"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
import * as sch from '../../utilities/json-schema';
|
import * as sch from '../../utilities/json-schema';
|
||||||
import { HttpConfig } from '../../http/server';
|
|
||||||
import { HttpWebDependencies } from '../server';
|
|
||||||
import { render_login_page } from './login-page';
|
import { render_login_page } from './login-page';
|
||||||
import { send_html_error } from '../../http/send-error';
|
import { send_html_error } from '../../http/send-error';
|
||||||
import { redirect_200_refresh } from '../../http/redirects';
|
import { redirect_200_refresh } from '../../http/redirects';
|
||||||
import { FastifyInstance, FastifyRequest, RouteShorthandOptions } from 'fastify';
|
|
||||||
|
import type { HttpConfig } from '../../http/server';
|
||||||
|
import type { HttpWebDependencies } from '../server';
|
||||||
|
import type { HttpURL, Locale, Timezone } from '../../utilities/types';
|
||||||
|
import type { FastifyInstance, FastifyRequest, RouteShorthandOptions } from 'fastify';
|
||||||
|
import { UserinfoResponse } from 'openid-client';
|
||||||
|
|
||||||
export function register_login_callback_endpoint(http_server: FastifyInstance, conf: HttpConfig, deps: HttpWebDependencies) {
|
export function register_login_callback_endpoint(http_server: FastifyInstance, conf: HttpConfig, deps: HttpWebDependencies) {
|
||||||
const { logger, pkce_cookie, oidc, storage, snowflake, session, argon2 } = deps;
|
const { logger, pkce_cookie, oidc, storage, snowflake, session, argon2 } = deps;
|
||||||
@ -77,24 +80,24 @@ export function register_login_callback_endpoint(http_server: FastifyInstance, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.debug({ oidc_subject: user_info.sub }, 'fetched user info; looking up local user data');
|
log.debug({ oidc_subject: user_info.sub }, 'fetched user info; looking up local user data');
|
||||||
|
log.debug({ user_info }, 'userinfo');
|
||||||
|
|
||||||
let user = await storage.get_user_by_oidc_sub(user_info.sub);
|
let user = await storage.get_user_by_oidc_sub(user_info.sub);
|
||||||
|
|
||||||
if (! user) {
|
if (! user) {
|
||||||
log.debug('user does not have a local profile; creating a new one');
|
log.debug('user does not have a local profile; creating a new one');
|
||||||
|
|
||||||
// todo: handle missing fields
|
|
||||||
// todo: handle non-unique usernames
|
// todo: handle non-unique usernames
|
||||||
user = {
|
user = {
|
||||||
id: snowflake.uid_str(),
|
id: snowflake.uid_str(),
|
||||||
oidc_subject: user_info.sub,
|
oidc_subject: user_info.sub,
|
||||||
username: user_info.preferred_username,
|
username: user_info.preferred_username,
|
||||||
locale: user_info.locale,
|
|
||||||
time_zone: user_info.zoneinfo,
|
|
||||||
name: user_info.name,
|
name: user_info.name,
|
||||||
picture: user_info.picture,
|
locale: get_locale(user_info),
|
||||||
profile: user_info.profile,
|
timezone: get_zoneinfo(user_info),
|
||||||
website: user_info.website,
|
picture: user_info.picture as HttpURL,
|
||||||
|
profile: user_info.profile as HttpURL,
|
||||||
|
website: user_info.website as HttpURL,
|
||||||
};
|
};
|
||||||
|
|
||||||
await storage.create_user(user);
|
await storage.create_user(user);
|
||||||
@ -111,3 +114,13 @@ export function register_login_callback_endpoint(http_server: FastifyInstance, c
|
|||||||
redirect_200_refresh(res, conf.exposed_url, 'Login successful');
|
redirect_200_refresh(res, conf.exposed_url, 'Login successful');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_locale(user_info: UserinfoResponse) : Locale {
|
||||||
|
const from_userinfo = user_info.locale?.replace('_', '-') as Locale;
|
||||||
|
return from_userinfo || 'en-US';
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_zoneinfo(user_info: UserinfoResponse) : Timezone {
|
||||||
|
const from_userinfo = user_info.zoneinfo as Timezone;
|
||||||
|
return from_userinfo || 'Africa/Abidjan';
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ async function main() {
|
|||||||
|
|
||||||
// Create the logger and storage
|
// Create the logger and storage
|
||||||
const logger = create_logger(conf.logging);
|
const logger = create_logger(conf.logging);
|
||||||
const storage = create_storage_provider(conf.storage);
|
const storage = create_storage_provider(conf.storage, logger.child({ logger: 'storage' }));
|
||||||
|
|
||||||
// Create the metadata server
|
// Create the metadata server
|
||||||
const http_meta = create_http_metadata_server(conf.http_meta, {
|
const http_meta = create_http_metadata_server(conf.http_meta, {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
import type { FileStorageConfig } from './file/config';
|
import type { FileStorageConfig } from './file/config';
|
||||||
// import type { SQLiteStorageConfig } from './sqlite/config';
|
import type { SQLite3StorageConfig } from './sqlite3/config';
|
||||||
// import type { MariaDBStorageConfig } from './mariadb/config';
|
// import type { MariaDBStorageConfig } from './mariadb/config';
|
||||||
|
|
||||||
export type StorageConfig
|
export type StorageConfig
|
||||||
= FileStorageConfig
|
= FileStorageConfig
|
||||||
// | SQLiteStorageConfig
|
| SQLite3StorageConfig
|
||||||
// | MariaDBStorageConfig
|
// | MariaDBStorageConfig
|
||||||
;
|
;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
|
import { pino } from 'pino';
|
||||||
import { make_data_dir } from '../files';
|
import { make_data_dir } from '../files';
|
||||||
import { StorageProvider } from '../provider';
|
import { StorageProvider } from '../provider';
|
||||||
import { FileStorageConfig } from './config';
|
import { FileStorageConfig } from './config';
|
||||||
import { create_session, get_session, delete_session, cleanup_old_sessions } from './sessions';
|
import { create_session, get_session, delete_session, cleanup_old_sessions } from './sessions';
|
||||||
import { get_user, create_user, update_user, delete_user, get_user_by_oidc_sub } from './users';
|
import { get_user, create_user, update_user, delete_user, get_user_by_oidc_sub } from './users';
|
||||||
|
|
||||||
export function create_file_storage_provider(conf: FileStorageConfig) : StorageProvider {
|
export function create_file_storage_provider(conf: FileStorageConfig, logger: pino.Logger) : StorageProvider {
|
||||||
let status: 'ok' | 'warning' | 'updating' | 'unavailable' = 'unavailable';
|
let status: 'ok' | 'warning' | 'updating' | 'unavailable' = 'unavailable';
|
||||||
|
|
||||||
const init_steps = [
|
const init_steps = [
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
import type { UserData } from '../provider';
|
import type { UserData } from '../provider';
|
||||||
import type { Snowflake } from '../../utilities/snowflake-uid';
|
import type { Snowflake } from '../../utilities/snowflake-uid';
|
||||||
import { delete_data_file, read_data_file, write_data_file } from '../files';
|
import { delete_data_file, read_data_file, write_data_file } from '../files';
|
||||||
|
import { memo } from '../../utilities/memo';
|
||||||
|
import { HttpURL, Locale, Timezone } from '../../utilities/types';
|
||||||
|
|
||||||
export async function get_user(user_id: Snowflake) : Promise<UserData> {
|
export async function get_user(user_id: Snowflake) : Promise<UserData> {
|
||||||
const data = await read_data_file<UserJson>(`users/${user_id}`, 'json');
|
const data = await read_data_file<UserJson>(`users/${user_id}`, 'json');
|
||||||
@ -32,11 +34,11 @@ interface UserJson {
|
|||||||
username: string;
|
username: string;
|
||||||
oidc_subject?: string;
|
oidc_subject?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
website?: string;
|
website?: HttpURL;
|
||||||
profile?: string;
|
profile?: HttpURL;
|
||||||
picture?: string;
|
picture?: HttpURL;
|
||||||
locale: string;
|
locale: Locale;
|
||||||
time_zone: string;
|
time_zone: Timezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
function to_json(user: UserData) : UserJson {
|
function to_json(user: UserData) : UserJson {
|
||||||
@ -46,7 +48,7 @@ function to_json(user: UserData) : UserJson {
|
|||||||
oidc_subject: user.oidc_subject,
|
oidc_subject: user.oidc_subject,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
locale: user.locale,
|
locale: user.locale,
|
||||||
time_zone: user.time_zone,
|
time_zone: user.timezone,
|
||||||
picture: user.picture,
|
picture: user.picture,
|
||||||
profile: user.profile,
|
profile: user.profile,
|
||||||
website: user.website,
|
website: user.website,
|
||||||
@ -60,7 +62,7 @@ function from_json(json: UserJson) : UserData {
|
|||||||
oidc_subject: json.oidc_subject,
|
oidc_subject: json.oidc_subject,
|
||||||
name: json.name,
|
name: json.name,
|
||||||
locale: json.locale,
|
locale: json.locale,
|
||||||
time_zone: json.time_zone,
|
timezone: json.time_zone,
|
||||||
picture: json.picture,
|
picture: json.picture,
|
||||||
profile: json.profile,
|
profile: json.profile,
|
||||||
website: json.website,
|
website: json.website,
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
import { StorageConfig } from './config';
|
import { StorageConfig } from './config';
|
||||||
import { StorageProvider } from './provider';
|
import { StorageProvider } from './provider';
|
||||||
import { create_file_storage_provider } from './file';
|
import { create_file_storage_provider } from './file';
|
||||||
// import { create_sqlite_storage_provider } from './sqlite';
|
import { create_sqlite3_storage_provider } from './sqlite3';
|
||||||
// import { create_mariadb_storage_provider } from './mariadb';
|
// import { create_mariadb_storage_provider } from './mariadb';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
|
||||||
export type * from './provider';
|
export type * from './provider';
|
||||||
|
|
||||||
export function create_storage_provider(conf: StorageConfig) : StorageProvider {
|
export function create_storage_provider(conf: StorageConfig, logger: pino.Logger) : StorageProvider {
|
||||||
switch (conf.engine) {
|
switch (conf.engine) {
|
||||||
case 'file': return create_file_storage_provider(conf);
|
case 'file': return create_file_storage_provider(conf, logger);
|
||||||
// case 'sqlite': return create_sqlite_storage_provider(conf);
|
case 'sqlite3': return create_sqlite3_storage_provider(conf, logger);
|
||||||
// case 'mariadb': return create_mariadb_storage_provider(conf);
|
// case 'mariadb': return create_mariadb_storage_provider(conf, logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import type { Snowflake } from '../utilities/snowflake-uid';
|
import type { Snowflake } from '../utilities/snowflake-uid';
|
||||||
|
import { HttpURL, Locale, Timezone } from '../utilities/types';
|
||||||
|
|
||||||
export interface StorageProvider {
|
export interface StorageProvider {
|
||||||
readonly ready: Promise<void | void[]>;
|
readonly ready: Promise<void | void[]>;
|
||||||
@ -23,7 +24,7 @@ export interface StorageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StorageStatus {
|
export interface StorageStatus {
|
||||||
engine: 'file' | 'mysql';
|
engine: 'file' | 'sqlite3' | 'mysql';
|
||||||
status: 'ok' | 'warning' | 'updating' | 'unavailable';
|
status: 'ok' | 'warning' | 'updating' | 'unavailable';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +41,9 @@ export interface UserData {
|
|||||||
username: string;
|
username: string;
|
||||||
oidc_subject?: string;
|
oidc_subject?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
website?: string;
|
website?: HttpURL;
|
||||||
profile?: string;
|
profile?: HttpURL;
|
||||||
picture?: string;
|
picture?: HttpURL;
|
||||||
locale: string;
|
locale: Locale;
|
||||||
time_zone: string;
|
timezone: Timezone;
|
||||||
}
|
}
|
||||||
|
6
src/storage/sqlite3/config.ts
Normal file
6
src/storage/sqlite3/config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export interface SQLite3StorageConfig {
|
||||||
|
engine: 'sqlite3';
|
||||||
|
pool_min: number;
|
||||||
|
pool_max: number;
|
||||||
|
}
|
17
src/storage/sqlite3/db.ts
Normal file
17
src/storage/sqlite3/db.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
import { pino } from 'pino';
|
||||||
|
import { Pool } from 'generic-pool';
|
||||||
|
import { SQLite3StorageConfig } from './config';
|
||||||
|
import { Database } from 'sqlite3';
|
||||||
|
|
||||||
|
export type DBPool = Pool<Database>;
|
||||||
|
|
||||||
|
export interface DB {
|
||||||
|
conf: SQLite3StorageConfig;
|
||||||
|
logger: pino.Logger;
|
||||||
|
|
||||||
|
// Database pools
|
||||||
|
settings: DBPool;
|
||||||
|
users: DBPool;
|
||||||
|
sessions: DBPool;
|
||||||
|
}
|
71
src/storage/sqlite3/index.ts
Normal file
71
src/storage/sqlite3/index.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
import { DB } from './db';
|
||||||
|
import { pino } from 'pino';
|
||||||
|
import { Pool } from 'generic-pool';
|
||||||
|
import { StorageProvider } from '../provider';
|
||||||
|
import { create_sqlite_pool } from './pool';
|
||||||
|
import { SQLite3StorageConfig } from './config';
|
||||||
|
import { bind_first_param } from '../../utilities/bind';
|
||||||
|
|
||||||
|
import { create_session, get_session, delete_session, cleanup_old_sessions } from './v1/sessions';
|
||||||
|
import { get_user, create_user, update_user, delete_user, get_user_by_oidc_sub } from './v1/users';
|
||||||
|
import { bring_db_schema_up_to_date } from './migrate';
|
||||||
|
|
||||||
|
export function create_sqlite3_storage_provider(conf: SQLite3StorageConfig, logger: pino.Logger) : StorageProvider {
|
||||||
|
let status: 'ok' | 'warning' | 'updating' | 'unavailable' = 'unavailable';
|
||||||
|
|
||||||
|
const db: DB = {
|
||||||
|
conf,
|
||||||
|
logger,
|
||||||
|
settings: null,
|
||||||
|
users: null,
|
||||||
|
sessions: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.settings = create_sqlite_pool('settings.db', db);
|
||||||
|
db.users = create_sqlite_pool('users.db', db);
|
||||||
|
db.sessions = create_sqlite_pool('sessions.db', db);
|
||||||
|
|
||||||
|
const ready = bring_db_schema_up_to_date(db).then(() => {
|
||||||
|
status = 'ok';
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready,
|
||||||
|
|
||||||
|
status() {
|
||||||
|
return {
|
||||||
|
engine: 'sqlite3',
|
||||||
|
status: status,
|
||||||
|
pools: {
|
||||||
|
settings: pool_stats(db.settings),
|
||||||
|
users: pool_stats(db.users),
|
||||||
|
sessions: pool_stats(db.sessions),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Login Sessions
|
||||||
|
get_session: bind_first_param(get_session, db),
|
||||||
|
create_session: bind_first_param(create_session, db),
|
||||||
|
delete_session: bind_first_param(delete_session, db),
|
||||||
|
cleanup_old_sessions: bind_first_param(cleanup_old_sessions, db),
|
||||||
|
|
||||||
|
// Users
|
||||||
|
get_user: bind_first_param(get_user, db),
|
||||||
|
get_user_by_oidc_sub: bind_first_param(get_user_by_oidc_sub, db),
|
||||||
|
create_user: bind_first_param(create_user, db),
|
||||||
|
update_user: bind_first_param(update_user, db),
|
||||||
|
delete_user: bind_first_param(delete_user, db),
|
||||||
|
};
|
||||||
|
|
||||||
|
function pool_stats(pool: Pool<any>) {
|
||||||
|
return {
|
||||||
|
min: pool.min,
|
||||||
|
max: pool.max,
|
||||||
|
available: pool.available,
|
||||||
|
borrowed: pool.borrowed,
|
||||||
|
pending: pool.pending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
131
src/storage/sqlite3/migrate.ts
Normal file
131
src/storage/sqlite3/migrate.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
import type { DB } from './db';
|
||||||
|
import type { ISOTimestamp } from '../../utilities/types';
|
||||||
|
import type * as sqlite3 from 'sqlite3';
|
||||||
|
|
||||||
|
import { get_one, run } from './run';
|
||||||
|
import { type } from './schema-utils';
|
||||||
|
import { migrate_v0_to_v1 } from './v1/migrate';
|
||||||
|
|
||||||
|
const supported_db_version = 1;
|
||||||
|
|
||||||
|
export let updating = false;
|
||||||
|
|
||||||
|
interface Migration {
|
||||||
|
(db: DB): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the database files exist and are updated to their latest schema versions
|
||||||
|
*/
|
||||||
|
export async function bring_db_schema_up_to_date(db: DB, no_update = false) {
|
||||||
|
updating = true;
|
||||||
|
const log = db.logger.child({ source: 'storage/sqlite3/migrate' });
|
||||||
|
const conn = await db.settings.acquire();
|
||||||
|
|
||||||
|
log.info(`checking for any needed db updates`);
|
||||||
|
|
||||||
|
await begin_transaction(db, conn);
|
||||||
|
await create_table_migration_history(db, conn);
|
||||||
|
|
||||||
|
let { version: db_version } = await get_current_schema_version(db, conn) || { version: 0 };
|
||||||
|
|
||||||
|
if (db_version === supported_db_version) {
|
||||||
|
log.info(`db already up to date`);
|
||||||
|
await commit(db, conn);
|
||||||
|
await db.settings.release(conn);
|
||||||
|
updating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db_version > supported_db_version || db_version < 0) {
|
||||||
|
console.error('settings.db seems to contain an unknown or unsupported version');
|
||||||
|
await commit(db, conn);
|
||||||
|
await db.settings.release(conn);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (no_update) {
|
||||||
|
console.error('settings.db is out of date, but no_update flag is set');
|
||||||
|
await commit(db, conn);
|
||||||
|
await db.settings.release(conn);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`db needs updates`, { db_version, new_version: supported_db_version });
|
||||||
|
|
||||||
|
while (db_version < supported_db_version) {
|
||||||
|
const now = (new Date).toISOString() as ISOTimestamp;
|
||||||
|
log.info(`performing update from v${db_version} to v${db_version + 1}...`);
|
||||||
|
await migrations[db_version](db);
|
||||||
|
db_version++;
|
||||||
|
await append_migration(db, conn, db_version, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
await commit(db, conn);
|
||||||
|
await db.settings.release(conn);
|
||||||
|
updating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrations: Migration[] = [
|
||||||
|
migrate_v0_to_v1,
|
||||||
|
// migrate_v1_to_v2,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Migration History Table =====
|
||||||
|
|
||||||
|
const sql_create_table_migration_history = `
|
||||||
|
create table if not exists migration_history (
|
||||||
|
id ${type.rowid} primary key,
|
||||||
|
version int not null,
|
||||||
|
migrated_at datetime not null
|
||||||
|
)`;
|
||||||
|
|
||||||
|
async function create_table_migration_history(db: DB, conn: sqlite3.Database) {
|
||||||
|
await run(db, conn, sql_create_table_migration_history);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_begin_transaction = `
|
||||||
|
begin transaction
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function begin_transaction(db: DB, conn: sqlite3.Database) {
|
||||||
|
await run(db, conn, sql_begin_transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_commit = `
|
||||||
|
commit
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function commit(db: DB, conn: sqlite3.Database) {
|
||||||
|
await run(db, conn, sql_commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_get_current_schema_version = `
|
||||||
|
select
|
||||||
|
hist.version as version
|
||||||
|
from migration_history hist
|
||||||
|
order by id desc
|
||||||
|
limit 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
function get_current_schema_version(db: DB, conn: sqlite3.Database) {
|
||||||
|
return get_one<{ version: number }>(db, conn, sql_get_current_schema_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_append_migration = `
|
||||||
|
insert into migration_history
|
||||||
|
(version, migrated_at)
|
||||||
|
values
|
||||||
|
(?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function append_migration(db: DB, conn: sqlite3.Database, version: number, migrated_at: ISOTimestamp) {
|
||||||
|
await run(db, conn, sql_append_migration, [ version, migrated_at ]);
|
||||||
|
}
|
91
src/storage/sqlite3/pool.ts
Normal file
91
src/storage/sqlite3/pool.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
import { DB } from './db';
|
||||||
|
import { run } from './run';
|
||||||
|
import { data_path } from '../files';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { createPool, Factory, Pool } from 'generic-pool';
|
||||||
|
import * as sqlite3 from 'sqlite3';
|
||||||
|
|
||||||
|
let next_conn_id = 1;
|
||||||
|
|
||||||
|
export const db_info_map = new Map<sqlite3.Database, DBInfo>();
|
||||||
|
|
||||||
|
export type DBPool = Pool<sqlite3.Database>;
|
||||||
|
|
||||||
|
export interface DBInfo {
|
||||||
|
file: string;
|
||||||
|
mode: number;
|
||||||
|
conn_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create_sqlite_pool(file: string, db: DB) : DBPool {
|
||||||
|
const path = data_path(file);
|
||||||
|
const mode = sqlite3.OPEN_CREATE | sqlite3.OPEN_READWRITE;
|
||||||
|
const factory: Factory<sqlite3.Database> = {
|
||||||
|
create() {
|
||||||
|
return open(db, path, mode);
|
||||||
|
},
|
||||||
|
destroy(conn: sqlite3.Database) {
|
||||||
|
return close(db, conn);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const pool = createPool(factory, {
|
||||||
|
min: db.conf.pool_min,
|
||||||
|
max: db.conf.pool_max,
|
||||||
|
});
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(db: DB, file: string, mode: number) : Promise<sqlite3.Database> {
|
||||||
|
const conn_id = next_conn_id++;
|
||||||
|
const db_info: DBInfo = { file, mode, conn_id };
|
||||||
|
const log = db.logger.child(db_info);
|
||||||
|
|
||||||
|
log.info('opening database connection');
|
||||||
|
|
||||||
|
return new Promise<sqlite3.Database>((resolve, reject) => {
|
||||||
|
const conn = new sqlite3.Database(file, mode, async (error) => {
|
||||||
|
if (error) {
|
||||||
|
log.error(error);
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
db_info_map.set(conn, db_info);
|
||||||
|
|
||||||
|
log.debug('database open');
|
||||||
|
log.debug('confirming file permissions = 0600');
|
||||||
|
|
||||||
|
// Ensure the file is not accessible to anyone but the server user
|
||||||
|
await fs.chmod(file, 0o600);
|
||||||
|
|
||||||
|
log.debug('enabling foreign keys');
|
||||||
|
|
||||||
|
// Enable foreign keys
|
||||||
|
await run(db, conn, 'PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
|
log.info('ready');
|
||||||
|
resolve(conn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(db: DB, conn: sqlite3.Database) : Promise<void> {
|
||||||
|
const db_info = db_info_map.get(conn);
|
||||||
|
const log = db.logger.child(db_info);
|
||||||
|
|
||||||
|
log.info('closing database connection');
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
conn.close((error) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('closed');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
83
src/storage/sqlite3/run.ts
Normal file
83
src/storage/sqlite3/run.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
import { DB } from './db';
|
||||||
|
import { db_info_map } from './pool';
|
||||||
|
import * as sqlite3 from 'sqlite3';
|
||||||
|
|
||||||
|
let next_query_id = 1;
|
||||||
|
|
||||||
|
export function run(db: DB, conn: sqlite3.Database, query: string, params: any[] | object = [ ]) {
|
||||||
|
const query_id = next_query_id++;
|
||||||
|
const info = Object.assign({ query_id }, db_info_map.get(conn));
|
||||||
|
const log = db.logger.child(info);
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
query = sql(query);
|
||||||
|
|
||||||
|
return new Promise<sqlite3.RunResult>((resolve, reject) => {
|
||||||
|
log.trace(`run: ${query}`);
|
||||||
|
|
||||||
|
conn.run(query, params, function(error) {
|
||||||
|
if (error) {
|
||||||
|
log.error('run failed: ' + error.stack);
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug({ duration: Date.now() - start }, `run complete: ${trunc(query)}`);
|
||||||
|
resolve(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_one<T>(db: DB, conn: sqlite3.Database, query: string, params: any[] | object = [ ]) {
|
||||||
|
const query_id = next_query_id++;
|
||||||
|
const info = Object.assign({ query_id }, db_info_map.get(conn));
|
||||||
|
const log = db.logger.child(info);
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
query = sql(query);
|
||||||
|
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
log.trace(`get_one: ${query}`);
|
||||||
|
|
||||||
|
conn.get(query, params, function(error, row) {
|
||||||
|
if (error) {
|
||||||
|
log.error('get_one failed: ' + error.stack);
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug({ duration: Date.now() - start }, `get_one complete: ${trunc(query)}`);
|
||||||
|
resolve(row as any);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_all<T>(db: DB, conn: sqlite3.Database, query: string, params: any[] | object = [ ]) {
|
||||||
|
const query_id = next_query_id++;
|
||||||
|
const info = Object.assign({ query_id }, db_info_map.get(conn));
|
||||||
|
const log = db.logger.child(info);
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
query = sql(query);
|
||||||
|
|
||||||
|
return new Promise<T[]>((resolve, reject) => {
|
||||||
|
log.trace(`get_all: ${query}`);
|
||||||
|
|
||||||
|
conn.all(query, params, function(error, rows) {
|
||||||
|
if (error) {
|
||||||
|
log.error('get_all failed: ' + error.stack);
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug({ duration: Date.now() - start }, `get_all complete: ${trunc(query)}`);
|
||||||
|
resolve(rows as any[]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function trunc(sql: string) {
|
||||||
|
return `${sql.slice(0, 20)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sql(query: string) {
|
||||||
|
return query.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
30
src/storage/sqlite3/schema-utils.ts
Normal file
30
src/storage/sqlite3/schema-utils.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
import { run } from './run';
|
||||||
|
import type { DB } from './db';
|
||||||
|
import type * as sqlite3 from 'sqlite3';
|
||||||
|
|
||||||
|
export const type = {
|
||||||
|
rowid: 'integer' as const,
|
||||||
|
url: 'varchar(1000)' as const,
|
||||||
|
snowflake: 'integer' as const,
|
||||||
|
uuid: 'varchar(50)' as const,
|
||||||
|
ref_value: 'varchar(64)' as const,
|
||||||
|
bool: 'tinyint unsigned' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function create_ref_table(db: DB, conn: sqlite3.Database, table_name: string, values: string[]) {
|
||||||
|
const sql_create_table = `
|
||||||
|
create table if not exists ${table_name} (
|
||||||
|
value ${type.ref_value} not null primary key
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sql_insert_values = `
|
||||||
|
insert into ${table_name}
|
||||||
|
(value)
|
||||||
|
values ${values.map(() => `(?)`).join(', ')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
await run(db, conn, sql_create_table);
|
||||||
|
await run(db, conn, sql_insert_values, values);
|
||||||
|
}
|
113
src/storage/sqlite3/v1/migrate.ts
Normal file
113
src/storage/sqlite3/v1/migrate.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
import { run } from '../run';
|
||||||
|
import { type } from '../schema-utils';
|
||||||
|
import type { DB } from '../db';
|
||||||
|
import type * as sqlite3 from 'sqlite3';
|
||||||
|
import type { Snowflake } from '../../../utilities/snowflake-uid';
|
||||||
|
|
||||||
|
export async function migrate_v0_to_v1(db: DB) {
|
||||||
|
await Promise.all([
|
||||||
|
// migrate_v1_settings(db),
|
||||||
|
migrate_v1_users(db),
|
||||||
|
migrate_v1_sessions(db),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Settings Database =====
|
||||||
|
|
||||||
|
async function migrate_v1_settings(db: DB) {
|
||||||
|
const conn = await db.settings.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await create_table_settings(db, conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
await db.settings.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_create_table_settings = `
|
||||||
|
create table if not exists settings (
|
||||||
|
//
|
||||||
|
)`;
|
||||||
|
|
||||||
|
async function create_table_settings(db: DB, conn: sqlite3.Database) {
|
||||||
|
await run(db, conn, sql_create_table_settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Users Database =====
|
||||||
|
|
||||||
|
async function migrate_v1_users(db: DB) {
|
||||||
|
const conn = await db.users.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await create_table_users(db, conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
await db.users.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_create_table_users = `
|
||||||
|
create table if not exists users (
|
||||||
|
id ${type.rowid} primary key,
|
||||||
|
username text not null,
|
||||||
|
oidc_subject text not null,
|
||||||
|
name text,
|
||||||
|
locale varchar(20) not null default 'en-US',
|
||||||
|
timezone text not null default 'Africa/Abidjan',
|
||||||
|
website ${type.url},
|
||||||
|
profile ${type.url},
|
||||||
|
picture ${type.url},
|
||||||
|
|
||||||
|
unique(username),
|
||||||
|
unique(oidc_subject)
|
||||||
|
)`;
|
||||||
|
|
||||||
|
async function create_table_users(db: DB, conn: sqlite3.Database) {
|
||||||
|
await run(db, conn, sql_create_table_users);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Sessions Database =====
|
||||||
|
|
||||||
|
async function migrate_v1_sessions(db: DB) {
|
||||||
|
const conn = await db.sessions.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await create_table_sessions(db, conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
await db.sessions.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionRow {
|
||||||
|
prefix: string;
|
||||||
|
key_hash: string;
|
||||||
|
user_id: Snowflake;
|
||||||
|
started: string;
|
||||||
|
expires: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_create_table_sessions = `
|
||||||
|
create table if not exists sessions (
|
||||||
|
prefix varchar(16) not null primary key,
|
||||||
|
key_hash text not null,
|
||||||
|
user_id ${type.rowid},
|
||||||
|
started datetime not null,
|
||||||
|
expires datetime not null
|
||||||
|
)`;
|
||||||
|
|
||||||
|
async function create_table_sessions(db: DB, conn: sqlite3.Database) {
|
||||||
|
await run(db, conn, sql_create_table_sessions);
|
||||||
|
}
|
||||||
|
|
113
src/storage/sqlite3/v1/sessions.ts
Normal file
113
src/storage/sqlite3/v1/sessions.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
import type { DB } from '../db';
|
||||||
|
import type { SessionData } from '../../provider';
|
||||||
|
import type { ISOTimestamp } from '../../../utilities/types';
|
||||||
|
import type { Snowflake } from '../../../utilities/snowflake-uid';
|
||||||
|
|
||||||
|
import { get_one, run } from '../run';
|
||||||
|
|
||||||
|
export interface SessionRow {
|
||||||
|
prefix: string;
|
||||||
|
key_hash: string;
|
||||||
|
user_id: Snowflake;
|
||||||
|
started: ISOTimestamp;
|
||||||
|
expires: ISOTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_get_session = `
|
||||||
|
select
|
||||||
|
session.prefix as prefix,
|
||||||
|
session.key_hash as key_hash,
|
||||||
|
session.user_id as user_id,
|
||||||
|
session.started as started,
|
||||||
|
session.expires as expires
|
||||||
|
from sessions session
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function get_session(db: DB, prefix: string) {
|
||||||
|
const conn = await db.sessions.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await get_one<SessionRow>(db, conn, sql_get_session + 'where session.prefix = ?', [ prefix ]);
|
||||||
|
|
||||||
|
if (! data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return from_db_row(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.sessions.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_create_session = `
|
||||||
|
insert into sessions
|
||||||
|
(prefix, key_hash, user_id, started, expires)
|
||||||
|
values
|
||||||
|
(?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function create_session(db: DB, data: SessionData) {
|
||||||
|
const conn = await db.sessions.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await run(db, conn, sql_create_session, [
|
||||||
|
data.prefix,
|
||||||
|
data.key_hash,
|
||||||
|
data.user_id,
|
||||||
|
data.started,
|
||||||
|
data.expires,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.sessions.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_delete_session = `
|
||||||
|
delete from sessions
|
||||||
|
where prefix = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function delete_session(db: DB, prefix: string) {
|
||||||
|
const conn = await db.sessions.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await run(db, conn, sql_delete_session, [ prefix ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.sessions.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_cleanup_old_sessions = `
|
||||||
|
delete from sessions
|
||||||
|
where expires <= ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function cleanup_old_sessions(db: DB) {
|
||||||
|
const now = (new Date).toISOString();
|
||||||
|
const conn = await db.sessions.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await run(db, conn, sql_cleanup_old_sessions, [ now ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.sessions.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function from_db_row(row: SessionRow) : SessionData {
|
||||||
|
return {
|
||||||
|
prefix: row.prefix,
|
||||||
|
key_hash: row.key_hash,
|
||||||
|
user_id: row.user_id,
|
||||||
|
started: new Date(Date.parse(row.started)),
|
||||||
|
expires: new Date(Date.parse(row.expires)),
|
||||||
|
};
|
||||||
|
}
|
0
src/storage/sqlite3/v1/settings.ts
Normal file
0
src/storage/sqlite3/v1/settings.ts
Normal file
133
src/storage/sqlite3/v1/users.ts
Normal file
133
src/storage/sqlite3/v1/users.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
|
||||||
|
import type { DB } from '../db';
|
||||||
|
import type { UserData } from '../../provider';
|
||||||
|
import type { Snowflake } from '../../../utilities/snowflake-uid';
|
||||||
|
import type { HttpURL, Locale, Timezone } from '../../../utilities/types';
|
||||||
|
|
||||||
|
import { get_one, run } from '../run';
|
||||||
|
|
||||||
|
export interface UserRow {
|
||||||
|
id: Snowflake;
|
||||||
|
username: string;
|
||||||
|
oidc_subject?: string;
|
||||||
|
name?: string;
|
||||||
|
website?: HttpURL;
|
||||||
|
profile?: HttpURL;
|
||||||
|
picture?: HttpURL;
|
||||||
|
locale: Locale;
|
||||||
|
timezone: Timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_get_user = `
|
||||||
|
select
|
||||||
|
user.id as id,
|
||||||
|
user.username as username,
|
||||||
|
user.oidc_subject as oidc_subject,
|
||||||
|
user.name as name,
|
||||||
|
user.website as website,
|
||||||
|
user.profile as profile,
|
||||||
|
user.picture as picture,
|
||||||
|
user.locale as locale,
|
||||||
|
user.timezone as timezone
|
||||||
|
from users user
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function get_user(db: DB, user_id: Snowflake) : Promise<UserData> {
|
||||||
|
const conn = await db.users.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await get_one<UserRow>(db, conn, sql_get_user + 'where user.id = ?', [ user_id ]);
|
||||||
|
|
||||||
|
if (! data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return from_db_row(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.users.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_user_by_oidc_sub(db: DB, user_sub: string) : Promise<UserData> {
|
||||||
|
const conn = await db.users.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await get_one<UserRow>(db, conn, sql_get_user + 'where user.oidc_subject = ?', [ user_sub ]);
|
||||||
|
|
||||||
|
if (! data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return from_db_row(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.users.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_create_user = `
|
||||||
|
insert into users
|
||||||
|
(id, username, oidc_subject, name, website, profile, picture, locale, timezone)
|
||||||
|
values
|
||||||
|
(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function create_user(db: DB, data: UserData) {
|
||||||
|
const conn = await db.users.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await run(db, conn, sql_create_user, [
|
||||||
|
data.id,
|
||||||
|
data.username,
|
||||||
|
data.oidc_subject,
|
||||||
|
data.name,
|
||||||
|
data.website,
|
||||||
|
data.profile,
|
||||||
|
data.picture,
|
||||||
|
data.locale,
|
||||||
|
data.timezone,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.users.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update_user(db: DB, user_id: Snowflake, data: UserData) {
|
||||||
|
// todo: update_user
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql_delete_user = `
|
||||||
|
delete from users
|
||||||
|
where id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
export async function delete_user(db: DB, user_id: Snowflake) {
|
||||||
|
const conn = await db.users.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await run(db, conn, sql_delete_user, [ user_id ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
db.users.release(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function from_db_row(json: UserRow) : UserData {
|
||||||
|
return {
|
||||||
|
id: json.id,
|
||||||
|
username: json.username,
|
||||||
|
oidc_subject: json.oidc_subject,
|
||||||
|
name: json.name,
|
||||||
|
locale: json.locale,
|
||||||
|
timezone: json.timezone,
|
||||||
|
picture: json.picture,
|
||||||
|
profile: json.profile,
|
||||||
|
website: json.website,
|
||||||
|
};
|
||||||
|
}
|
8
src/utilities/bind.ts
Normal file
8
src/utilities/bind.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
import type { FirstParam, Func, NonFirstParams } from './types';
|
||||||
|
|
||||||
|
export function bind_first_param<F extends Func>(func: F, first: FirstParam<F>) : (...args: NonFirstParams<F>) => ReturnType<F> {
|
||||||
|
return function (...args: NonFirstParams<F>) : ReturnType<F> {
|
||||||
|
return func(first, ...args);
|
||||||
|
};
|
||||||
|
}
|
@ -2,3 +2,23 @@
|
|||||||
export type Func<T extends any = any> = (...args: any[]) => T;
|
export type Func<T extends any = any> = (...args: any[]) => T;
|
||||||
|
|
||||||
export type Params<T extends Func> = T extends (...args: infer P) => any ? P : never;
|
export type Params<T extends Func> = T extends (...args: infer P) => any ? P : never;
|
||||||
|
|
||||||
|
export type FirstParam<T extends Func> = T extends (first: infer F, ...args: infer P) => any ? F : never;
|
||||||
|
|
||||||
|
export type NonFirstParams<T extends Func> = T extends (first: infer F, ...args: infer P) => any ? P : never;
|
||||||
|
|
||||||
|
export type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||||
|
|
||||||
|
export type ISODate = `${number}-${number}-${number}`;
|
||||||
|
|
||||||
|
export type ISOTime = `${number}:${number}:${number}.${number}`;
|
||||||
|
|
||||||
|
export type ISOZoneOffset = 'Z' | `${'+' | '-' | ''}${number}:${number}`;
|
||||||
|
|
||||||
|
export type ISOTimestamp = `${ISODate}T${ISOTime}${ISOZoneOffset}`;
|
||||||
|
|
||||||
|
export type Timezone = `${string}/${string}`;
|
||||||
|
|
||||||
|
export type Locale = `${string}-${string}`;
|
||||||
|
|
||||||
|
export type HttpURL = `http${'s' | ''}://${string}`;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user