build sqlite3 store

This commit is contained in:
James Brumond 2023-07-23 19:43:05 -07:00
parent 7addce60bb
commit dc6e01db14
Signed by: james
GPG Key ID: E8F2FC44BAA3357A
23 changed files with 2120 additions and 33 deletions

1224
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,9 @@
"url": "git@git.jbrumond.me:templates/nodejs-typescript-service.git"
},
"scripts": {
"tsc": "tsc --build"
"tsc": "tsc --build",
"clean:build": "rm -rf ./build/*",
"clean:data": "rm -rf ./data/*"
},
"author": "James Brumond <https://jbrumond.me>",
"license": "ISC",
@ -26,9 +28,11 @@
"argon2": "^0.30.3",
"fast-xml-parser": "^4.2.6",
"fastify": "^4.19.2",
"generic-pool": "^3.9.0",
"luxon": "^3.3.0",
"openid-client": "^5.4.3",
"pino": "^8.14.1",
"sqlite3": "^5.1.6",
"yaml": "^2.3.1"
}
}

View File

@ -219,7 +219,8 @@
"title": "Storage Config",
"description": "Configuration for the main application data storage layer",
"oneOf": [
{ "$ref": "#/$defs/file_storage_config" }
{ "$ref": "#/$defs/file_storage_config" },
{ "$ref": "#/$defs/sqlite3_storage_config" }
]
}
},
@ -248,6 +249,30 @@
"required": [
"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"
]
}
}
}

View File

@ -1,11 +1,14 @@
import * as sch from '../../utilities/json-schema';
import { HttpConfig } from '../../http/server';
import { HttpWebDependencies } from '../server';
import { render_login_page } from './login-page';
import { send_html_error } from '../../http/send-error';
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) {
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({ user_info }, 'userinfo');
let user = await storage.get_user_by_oidc_sub(user_info.sub);
if (! user) {
log.debug('user does not have a local profile; creating a new one');
// todo: handle missing fields
// todo: handle non-unique usernames
user = {
id: snowflake.uid_str(),
oidc_subject: user_info.sub,
username: user_info.preferred_username,
locale: user_info.locale,
time_zone: user_info.zoneinfo,
name: user_info.name,
picture: user_info.picture,
profile: user_info.profile,
website: user_info.website,
locale: get_locale(user_info),
timezone: get_zoneinfo(user_info),
picture: user_info.picture as HttpURL,
profile: user_info.profile as HttpURL,
website: user_info.website as HttpURL,
};
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');
});
}
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';
}

View File

@ -21,7 +21,7 @@ async function main() {
// Create the logger and storage
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
const http_meta = create_http_metadata_server(conf.http_meta, {

View File

@ -1,10 +1,10 @@
import type { FileStorageConfig } from './file/config';
// import type { SQLiteStorageConfig } from './sqlite/config';
import type { SQLite3StorageConfig } from './sqlite3/config';
// import type { MariaDBStorageConfig } from './mariadb/config';
export type StorageConfig
= FileStorageConfig
// | SQLiteStorageConfig
| SQLite3StorageConfig
// | MariaDBStorageConfig
;

View File

@ -1,11 +1,12 @@
import { pino } from 'pino';
import { make_data_dir } from '../files';
import { StorageProvider } from '../provider';
import { FileStorageConfig } from './config';
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';
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';
const init_steps = [

View File

@ -2,6 +2,8 @@
import type { UserData } from '../provider';
import type { Snowflake } from '../../utilities/snowflake-uid';
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> {
const data = await read_data_file<UserJson>(`users/${user_id}`, 'json');
@ -32,11 +34,11 @@ interface UserJson {
username: string;
oidc_subject?: string;
name?: string;
website?: string;
profile?: string;
picture?: string;
locale: string;
time_zone: string;
website?: HttpURL;
profile?: HttpURL;
picture?: HttpURL;
locale: Locale;
time_zone: Timezone;
}
function to_json(user: UserData) : UserJson {
@ -46,7 +48,7 @@ function to_json(user: UserData) : UserJson {
oidc_subject: user.oidc_subject,
name: user.name,
locale: user.locale,
time_zone: user.time_zone,
time_zone: user.timezone,
picture: user.picture,
profile: user.profile,
website: user.website,
@ -60,7 +62,7 @@ function from_json(json: UserJson) : UserData {
oidc_subject: json.oidc_subject,
name: json.name,
locale: json.locale,
time_zone: json.time_zone,
timezone: json.time_zone,
picture: json.picture,
profile: json.profile,
website: json.website,

View File

@ -2,15 +2,16 @@
import { StorageConfig } from './config';
import { StorageProvider } from './provider';
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 { pino } from 'pino';
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) {
case 'file': return create_file_storage_provider(conf);
// case 'sqlite': return create_sqlite_storage_provider(conf);
// case 'mariadb': return create_mariadb_storage_provider(conf);
case 'file': return create_file_storage_provider(conf, logger);
case 'sqlite3': return create_sqlite3_storage_provider(conf, logger);
// case 'mariadb': return create_mariadb_storage_provider(conf, logger);
}
}

View File

@ -1,5 +1,6 @@
import type { Snowflake } from '../utilities/snowflake-uid';
import { HttpURL, Locale, Timezone } from '../utilities/types';
export interface StorageProvider {
readonly ready: Promise<void | void[]>;
@ -23,7 +24,7 @@ export interface StorageProvider {
}
export interface StorageStatus {
engine: 'file' | 'mysql';
engine: 'file' | 'sqlite3' | 'mysql';
status: 'ok' | 'warning' | 'updating' | 'unavailable';
}
@ -40,9 +41,9 @@ export interface UserData {
username: string;
oidc_subject?: string;
name?: string;
website?: string;
profile?: string;
picture?: string;
locale: string;
time_zone: string;
website?: HttpURL;
profile?: HttpURL;
picture?: HttpURL;
locale: Locale;
timezone: Timezone;
}

View File

@ -0,0 +1,6 @@
export interface SQLite3StorageConfig {
engine: 'sqlite3';
pool_min: number;
pool_max: number;
}

17
src/storage/sqlite3/db.ts Normal file
View 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;
}

View 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,
};
}
}

View 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 ]);
}

View 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();
});
});
}

View 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();
}

View 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);
}

View 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);
}

View 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)),
};
}

View File

View 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
View 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);
};
}

View File

@ -2,3 +2,23 @@
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 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}`;