import type { Config } from '../config-loader/config-loader.js'; import type { AnyObject } from '../sbvr-api/common-types.js'; import { Store } from 'express-session'; import * as permissions from '../sbvr-api/permissions.js'; import { api } from '../sbvr-api/sbvr-utils.js'; export { Store }; const sessionModel = ` Vocabulary: session Term: session id Concept Type: Short Text (Type) Term: data Concept Type: JSON (Type) Term: expiry time Concept type: Date Time (Type) Term: session Database ID Field: session id Reference Scheme: session id Fact type: session has data Necessity: Each session has exactly 1 data Fact type: session has session id Necessity: Each session has exactly 1 session id Necessity: Each session id is of exactly 1 session Fact type: session has expiry time Necessity: Each session has at most 1 expiry time `; const asCallback = async ( callback: undefined | ((err: any, result?: T) => void), promise: Promise, ) => { let err; let result; try { result = await promise; } catch ($err) { err = $err; } try { callback?.(err, result); } catch { // ignore errors in the callback } }; export class PinejsSessionStore extends Store { public get = ((sid, callback) => { void asCallback( callback, api.session .get({ resource: 'session', id: sid, passthrough: { req: permissions.rootRead, }, options: { $select: 'data', }, }) .then((session: AnyObject) => { if (session != null) { return session.data; } }), ); }) as Store['get']; public set = ((sid, data, callback) => { const body = { session_id: sid, data, expiry_time: data?.cookie?.expires ?? null, }; void asCallback( callback, api.session.put({ resource: 'session', id: sid, passthrough: { req: permissions.root, }, body, }), ); }) as Store['set']; public destroy = ((sid, callback) => { void asCallback( callback, api.session.delete({ resource: 'session', id: sid, passthrough: { req: permissions.root, }, }), ); }) as Store['destroy']; public all = ((callback) => { void asCallback( callback, api.session .get({ resource: 'session', passthrough: { req: permissions.root, }, options: { $select: 'session_id', $filter: { expiry_time: { $ge: Date.now() }, }, }, }) .then((sessions: AnyObject[]) => sessions.map((s) => s.session_id)), ); }) as Store['all']; public clear = ((callback) => { void asCallback( callback, // TODO: Use a truncate api.session.delete({ resource: 'session', passthrough: { req: permissions.root, }, }), ); }) as Store['clear']; public length = ((callback) => { void asCallback( callback, api.session.get({ resource: 'session/', passthrough: { req: permissions.rootRead, }, options: { $count: { $filter: { expiry_time: { $ge: Date.now(), }, }, }, }, }), ); }) as Store['length']; public static config: Config = { models: [ { modelName: 'session', modelText: sessionModel, apiRoot: 'session', logging: { default: false, error: true, }, migrations: { '11.0.0-modified-at': ` ALTER TABLE "session" ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; `, '15.0.0-data-types': async (tx, sbvrUtils) => { switch (sbvrUtils.db.engine) { case 'mysql': await tx.executeSql(`\ ALTER TABLE "session" MODIFY "data" JSON NOT NULL;`); break; case 'postgres': await tx.executeSql(`\ ALTER TABLE "session" ALTER COLUMN "data" SET DATA TYPE JSONB USING "data"::JSONB;`); break; // No need to migrate for websql } }, }, }, ], }; }