@elysiumjs/mnemosyne-drizzle 0.2.0 → 0.4.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/CHANGELOG.md +112 -0
- package/dist/cache.d.ts +60 -0
- package/dist/commands/migration_run.command.d.ts +13 -0
- package/dist/commands/project.d.ts +11 -0
- package/dist/database.d.ts +82 -31
- package/dist/driver.d.ts +39 -0
- package/dist/drivers/bun-sql.d.ts +25 -0
- package/dist/drivers/bun-sql.js +1 -0
- package/dist/drivers/hyperdrive.d.ts +46 -0
- package/dist/drivers/hyperdrive.js +1 -0
- package/dist/drivers/node-postgres.d.ts +23 -0
- package/dist/drivers/node-postgres.js +1 -0
- package/dist/drivers/postgres-js.d.ts +25 -0
- package/dist/drivers/postgres-js.js +1 -0
- package/dist/drizzle.module.d.ts +3 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +11 -1
- package/dist/migrations.d.ts +12 -0
- package/dist/model.d.ts +9 -9
- package/dist/query.d.ts +28 -0
- package/dist/repository.d.ts +34 -6
- package/dist/tenancy.d.ts +25 -8
- package/package.json +63 -11
- package/.omc/state/agent-replay-3920b579-6d8e-4f5f-924a-319202936685.jsonl +0 -7
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/last-tool-error-state.json +0 -7
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/mission-state.json +0 -93
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/pre-tool-advisory-throttle.json +0 -18
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/subagent-tracking-state.json +0 -61
- package/.turbo/turbo-build.log +0 -3
- package/dist/commands/migration_generate.command.js +0 -1
- package/dist/commands/migration_run.command.js +0 -15
- package/dist/database.js +0 -1
- package/dist/drizzle.module.js +0 -1
- package/dist/migrations.js +0 -1
- package/dist/model.js +0 -1
- package/dist/repository.js +0 -1
- package/dist/table.js +0 -1
- package/dist/tenancy.js +0 -1
- package/scripts/minify.ts +0 -10
- package/src/commands/migration_generate.command.ts +0 -101
- package/src/commands/migration_run.command.ts +0 -160
- package/src/database.ts +0 -205
- package/src/drizzle.module.ts +0 -97
- package/src/index.ts +0 -47
- package/src/migrations.ts +0 -100
- package/src/model.ts +0 -178
- package/src/repository.ts +0 -113
- package/src/table.ts +0 -70
- package/src/tenancy.ts +0 -249
- package/tests/drizzle.module.test.ts +0 -201
- package/tests/migrations.test.ts +0 -123
- package/tests/model.test.ts +0 -362
- package/tests/setup.ts +0 -0
- package/tests/table.test.ts +0 -72
- package/tests/tenancy.test.ts +0 -212
- package/tsconfig.json +0 -12
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
var __decorate=function(decorators,target,key,desc){var c=arguments.length,r=c<3?target:desc===null?desc=Object.getOwnPropertyDescriptor(target,key):desc,d;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")r=Reflect.decorate(decorators,target,key,desc);else for(var i=decorators.length-1;i>=0;i--)if(d=decorators[i])r=(c<3?d(r):c>3?d(target,key,r):d(target,key))||r;return c>3&&r&&Object.defineProperty(target,key,r),r};var __metadata=function(k,v){if(typeof Reflect==="object"&&typeof Reflect.metadata==="function")return Reflect.metadata(k,v)};import{Command,CommandArgumentType}from"@elysiumjs/core";import prompts from"prompts";import{Database}from"../database";import{getMigrationName,planMigrations}from"../migrations";let MigrationRunCommand=class MigrationRunCommand extends Command{static command="migration:run";static description="Runs pending migrations.";tenant="";connection="default";fresh=false;async run(){if(!Database.connectionExists(this.connection)){this.error(`Connection ${this.bold(this.connection)} does not exist.`);return process.exit(1)}const sql=Database.getConnection(this.connection).$client;await sql.connect();await sql.begin(async(sql)=>{if(this.fresh){await sql`DROP SCHEMA IF EXISTS ${sql(this.tenant)} CASCADE;`;await sql`CREATE SCHEMA ${sql(this.tenant)};`}const[{schema_exists}]=await sql`SELECT EXISTS (
|
|
2
|
-
SELECT schema_name
|
|
3
|
-
FROM information_schema.schemata
|
|
4
|
-
WHERE schema_name = ${this.tenant}
|
|
5
|
-
) AS schema_exists;`;if(!schema_exists){const answers=await prompts([{type:"confirm",name:"create",message:`Schema ${this.tenant} does not exist. Do you want to create it?`,initial:true}],{onCancel:()=>{this.error("Operation cancelled.");process.exit(0)}});if(answers.create){await sql`CREATE SCHEMA ${sql(this.tenant)};`}else{this.error(`Schema ${this.bold(this.tenant)} creation cancelled.`);return process.exit(1)}}await sql`SET search_path TO ${sql(this.tenant)};`;const[{table_exists}]=await sql`SELECT EXISTS (
|
|
6
|
-
SELECT *
|
|
7
|
-
FROM information_schema.tables
|
|
8
|
-
WHERE table_name = 'elysium_migrations'
|
|
9
|
-
AND table_schema = ${this.tenant}
|
|
10
|
-
) AS table_exists;`;if(!table_exists){await sql`CREATE TABLE elysium_migrations (
|
|
11
|
-
id SERIAL PRIMARY KEY,
|
|
12
|
-
name TEXT NOT NULL,
|
|
13
|
-
batch INTEGER NOT NULL,
|
|
14
|
-
applied_at TIMESTAMP NOT NULL
|
|
15
|
-
);`}const rows=await sql`SELECT name, applied_at FROM elysium_migrations;`;const completedMigrations=rows.map((row)=>({name:row.name,appliedAt:row.applied_at}));const filesIt=new Bun.Glob(`./src/database/migrations/**/*.sql`).scan({absolute:true,onlyFiles:true});const files=await Array.fromAsync(filesIt);const{pending:sqlFiles,renames}=planMigrations(files,completedMigrations);for(const{from,to}of renames){await sql`UPDATE elysium_migrations SET name = ${to} WHERE name = ${from};`;this.info(`Migration ${this.bold(from)} is now tracked as ${this.bold(to)}.`)}if(sqlFiles.length===0){this.success("No new migrations to apply.");return}const s=this.spinner(`Running migrations for tenant ${this.bold(this.tenant)}...`);const[{max}]=await sql`SELECT MAX(batch) + 1 AS max FROM elysium_migrations;`;const batch=max??0;for(const path of sqlFiles){await sql.file(path);const[{name}]=await sql`INSERT INTO elysium_migrations (name, batch, applied_at) VALUES (${getMigrationName(path)}, ${batch}, ${new Date}) RETURNING name;`;s.pause(`Migration ${this.bold(name)} applied successfully.`)}s.complete(`Migrations for tenant ${this.bold(this.tenant)} applied successfully.`)});await sql.close()}};__decorate([Command.arg({description:"The name of the tenant in which the migrations will be executed.",type:CommandArgumentType.STRING,required:true}),__metadata("design:type",String)],MigrationRunCommand.prototype,"tenant",undefined);__decorate([Command.arg({description:"The name of the connection to use.",type:CommandArgumentType.STRING,default:"default"}),__metadata("design:type",String)],MigrationRunCommand.prototype,"connection",undefined);__decorate([Command.arg({description:"Drop the entire schema before running the migrations.",type:CommandArgumentType.BOOLEAN,default:false}),__metadata("design:type",Boolean)],MigrationRunCommand.prototype,"fresh",undefined);MigrationRunCommand=__decorate([Command.register()],MigrationRunCommand);export{MigrationRunCommand};
|
package/dist/database.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Service,ServiceNotReadyError}from"@elysiumjs/core";import{AbstractDatabase,createDatabaseCacheStorage}from"@elysiumjs/mnemosyne";import{getTableName,is,Table}from"drizzle-orm";import{drizzle}from"drizzle-orm/bun-sql";import{Cache as DrizzleCache}from"drizzle-orm/cache/core";export class DrizzleDatabaseCache extends DrizzleCache{cacheStrategy;globalTtl;#storageEngine;#cache;usedTablesPerKey={};constructor(cacheStrategy="explicit",globalTtl=1,storageEngine="redis"){super();this.cacheStrategy=cacheStrategy;this.globalTtl=globalTtl;this.#storageEngine=storageEngine}async#storage(){if(this.#cache)return this.#cache;try{this.#cache=createDatabaseCacheStorage({storage:this.#storageEngine})}catch(error){if(!(error instanceof ServiceNotReadyError))throw error;await Service.resolve(error.service);this.#cache=createDatabaseCacheStorage({storage:this.#storageEngine})}return this.#cache}strategy(){return this.cacheStrategy}async get(key){return await(await this.#storage()).get(key)??undefined}async put(key,response,tables,_isTag,config){await(await this.#storage()).set(key,response,(config?.ex??this.globalTtl)*1000);for(const table of tables){const keys=this.usedTablesPerKey[table];if(keys===undefined){this.usedTablesPerKey[table]=[key]}else{keys.push(key)}}}async onMutate(params){const tagsArray=params.tags?Array.isArray(params.tags)?params.tags:[params.tags]:[];const tablesArray=params.tables?Array.isArray(params.tables)?params.tables:[params.tables]:[];const keysToDelete=new Set;for(const table of tablesArray){const tableName=is(table,Table)?getTableName(table):table;const keys=this.usedTablesPerKey[tableName]??[];for(const key of keys)keysToDelete.add(key)}if(keysToDelete.size>0||tagsArray.length>0){const cache=await this.#storage();await cache.mdel(tagsArray);await cache.mdel([...keysToDelete]);for(const table of tablesArray){const tableName=is(table,Table)?getTableName(table):table;this.usedTablesPerKey[tableName]=[]}}}}class DrizzleDatabase extends AbstractDatabase{createConnection(props){return this.driver.createConnection(props)}driver={createConnection(config){const cache=config.cache===undefined?undefined:config.cache===true?new DrizzleDatabaseCache:config.cache.enabled!==false?new DrizzleDatabaseCache(config.cache.strategy,config.cache.ttl,config.cache.storage):undefined;return drizzle({...config,cache})},withTransaction(connection,callback){return connection.transaction((tx)=>callback(tx))},getRawClient(connection){return connection.$client}}}export const Database=new DrizzleDatabase;
|
package/dist/drizzle.module.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var __decorate=function(decorators,target,key,desc){var c=arguments.length,r=c<3?target:desc===null?desc=Object.getOwnPropertyDescriptor(target,key):desc,d;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")r=Reflect.decorate(decorators,target,key,desc);else for(var i=decorators.length-1;i>=0;i--)if(d=decorators[i])r=(c<3?d(r):c>3?d(target,key,r):d(target,key))||r;return c>3&&r&&Object.defineProperty(target,key,r),r};import{Module,needsDefaultAlias,runAll,Service}from"@elysiumjs/core";import{AbstractDatabase,MnemosyneModule,TENANCY_CONFIG_SERVICE,TENANCY_STRATEGY_SERVICE}from"@elysiumjs/mnemosyne";import{MigrationGenerateCommand}from"./commands/migration_generate.command";import{MigrationRunCommand}from"./commands/migration_run.command";import{Database}from"./database";import{DrizzleRLSTenancy,DrizzleSchemaTenancy}from"./tenancy";let DrizzleModule=class DrizzleModule extends Module.configurable({defaults:{default:"default"},required:["connections"]}){async onDestroy(){const names=Object.keys(this.config.connections).map(AbstractDatabase.getConnectionName);await runAll(names.filter((name)=>Service.isConstructed(name)).map((name)=>()=>Service.get(name)?.$client.close()))}};DrizzleModule=__decorate([Module.register({imports:[MnemosyneModule],commands:[MigrationGenerateCommand,MigrationRunCommand],services:(config)=>{const alias=needsDefaultAlias("DrizzleModule",config);const tenancy=config.tenancy??{mode:"schema"};return[...Object.entries(config.connections).map(([name,props])=>({name:AbstractDatabase.getConnectionName(name),global:true,factory:()=>Database.createConnection(props)})),...alias?[{name:AbstractDatabase.getConnectionName("default"),global:true,factory:(_config,get)=>get(AbstractDatabase.getConnectionName(config.default))}]:[],{name:TENANCY_CONFIG_SERVICE,global:true,factory:()=>tenancy},{name:TENANCY_STRATEGY_SERVICE,global:true,factory:()=>tenancy.mode==="rls"?new DrizzleRLSTenancy:new DrizzleSchemaTenancy}]}})],DrizzleModule);export{DrizzleModule};
|
package/dist/migrations.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{basename,dirname}from"node:path";const FOLDER_MIGRATION_FILE="migration.sql";export const getMigrationName=(path)=>basename(path)===FOLDER_MIGRATION_FILE?basename(dirname(path)):basename(path);const descriptiveName=(name)=>name.replace(/\.sql$/,"").split("_").slice(1).join("_");const folderTime=(name)=>{const match=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name);return match?Date.UTC(+match[1],+match[2]-1,+match[3],+match[4],+match[5],+match[6]):undefined};export const planMigrations=(files,applied)=>{const appliedNames=new Set(applied.map(({name})=>name));const fileNames=new Set(files.map(getMigrationName));const moved=new Map;for(const record of[...applied].sort((a,b)=>a.name.localeCompare(b.name))){if(!record.name.endsWith(".sql")||fileNames.has(record.name))continue;const key=descriptiveName(record.name);moved.set(key,[...moved.get(key)??[],record])}const plan={pending:[],renames:[]};for(const file of[...files].sort()){const name=getMigrationName(file);if(appliedNames.has(name))continue;const from=basename(file)===FOLDER_MIGRATION_FILE?takeMoved(moved,name):undefined;if(from)plan.renames.push({from,to:name});else plan.pending.push(file)}return plan};const takeMoved=(moved,name)=>{const records=moved.get(descriptiveName(name));const generatedAt=folderTime(name);const index=records?.findIndex(({appliedAt})=>generatedAt===undefined||generatedAt<=appliedAt.getTime())??-1;return index<0?undefined:records.splice(index,1)[0].name};
|
package/dist/model.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{AbstractModel,createSchemaFromModel}from"@elysiumjs/mnemosyne";import{getTableConfig}from"drizzle-orm/pg-core";import{getTableBuilders}from"./table";import{wrapTenantSchema}from"./tenancy";const mapDrizzleDataType=(col)=>{if(col.columnType==="PgUUID")return"uuid";if(col.dimensions)return"array";const[kind,constraint]=col.dataType.split(" ");switch(kind){case"number":return"number";case"boolean":return"boolean";case"array":return"array";case"bigint":return"bigint";case"object":switch(constraint){case"json":return"json";case"date":return"date";case"buffer":return"buffer";default:return"string"}default:return"string"}};export const drizzleAdapter={getTableName(table){return getTableConfig(table).name},getColumns(table){const config=getTableConfig(table);return config.columns.map((col)=>({name:col.name,dataType:mapDrizzleDataType(col),nullable:!col.notNull,hasDefault:col.hasDefault,isPrimaryKey:col.primary||config.primaryKeys.some((pk)=>pk.columns.some((c)=>c.name===col.name))}))},createTenantTable(table,tenant){const builders=getTableBuilders(table);if(!builders){throw new Error(`No column builders registered for table. `+`Use pgTable() from '@elysiumjs/mnemosyne-drizzle' for tenant support.`)}const config=getTableConfig(table);return wrapTenantSchema(tenant,config.name,builders)}};export const createSchemaFromDrizzle=(table,opts)=>{return createSchemaFromModel(drizzleAdapter.getColumns(table),opts)};export const DrizzleModel=(table,options)=>{const config=getTableConfig(table);const builders=getTableBuilders(table);const Base=AbstractModel(config.name,builders??{},drizzleAdapter,table);if(options?.supportTenancy!==undefined){Base.supportTenancy=options.supportTenancy}return Base};
|
package/dist/repository.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{AbstractRepository}from"@elysiumjs/mnemosyne";import{eq}from"drizzle-orm";import{Database}from"./database";export const DrizzleRepository=(model)=>{class R extends AbstractRepository(model,Database){async all(){return await this.db.select().from(model.table)}async paginate(count,page=1){const total=await this.db.$count(model.table);const offset=Math.max(page-1,0)*count;return{page,total,data:await this.db.select().from(model.table).offset(offset).limit(count)}}async find(id){const[row]=await this.db.select().from(model.table).where(eq(model.table.id,id));return row??null}async findBy(column,value){const[row]=await this.db.select().from(model.table).where(eq(model.table[column],value));return row??null}async insert(data){const[row]=await this.db.insert(model.table).values(data).returning();return row}async update(id,data){const[row]=await this.db.update(model.table).set(data).where(eq(model.table.id,id)).returning();return row}async updateAll(data){return await this.db.update(model.table).set(data).returning()}async delete(id){const[row]=await this.db.delete(model.table).where(eq(model.table.id,id)).returning();return row}async deleteAll(){return await this.db.delete(model.table).returning()}async exists(id){const count=await this.db.$count(model.table,eq(model.table.id,id));return count>0}}return R};
|
package/dist/table.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{pgTable as drizzlePgTable}from"drizzle-orm/pg-core";const tableRegistry=new WeakMap;export const getTableBuilders=(table)=>{return tableRegistry.get(table)};export const pgTable=(tableName,columns,extraConfig)=>{const table=drizzlePgTable(tableName,columns,extraConfig);tableRegistry.set(table,columns);return table};
|
package/dist/tenancy.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{getTenancyConfig}from"@elysiumjs/mnemosyne";import{sql}from"drizzle-orm";import{getTableConfig,pgPolicy,pgSchema,pgTable}from"drizzle-orm/pg-core";import{Database}from"./database";import{getTableBuilders}from"./table";const tenantSchemas=new Map;const schemaRegistry=new Map;export const getTenantSchema=(tenant)=>{return schemaRegistry.get(tenant)??registerTenantSchema(tenant)};export const registerTenantSchema=(tenant)=>{if(schemaRegistry.has(tenant)){return schemaRegistry.get(tenant)}const tenantSchema=pgSchema(tenant);schemaRegistry.set(tenant,tenantSchema);return tenantSchema};export const wrapTenantSchema=(tenant,tableName,columns)=>{if(tenant==="public"){return pgTable(tableName,columns)}const fullTableName=`${tenant}.${tableName}`;if(tenantSchemas.has(fullTableName)){return tenantSchemas.get(fullTableName)}const tenantSchema=getTenantSchema(tenant);const schemaTable=tenantSchema.table(tableName,columns);tenantSchemas.set(fullTableName,schemaTable);return schemaTable};export const withRLS=async(tenant,callback,connectionName="default")=>{const db=Database.getConnection(connectionName);const config=getTenancyConfig();const varName=config.rls?.sessionVariable??"app.current_tenant";const value=tenant??"public";return db.transaction(async(tx)=>{await tx.execute(sql`SELECT set_config(${varName}, ${value}, true)`);return callback(tx)})};export const setConnectionTenant=async(tenant,connectionName="default")=>{const db=Database.getConnection(connectionName);const config=getTenancyConfig();const varName=config.rls?.sessionVariable??"app.current_tenant";const value=tenant??"public";await db.execute(sql`SELECT set_config(${varName}, ${value}, false)`)};export const getSessionTenant=async(connectionName="default")=>{const db=Database.getConnection(connectionName);const config=getTenancyConfig();const varName=config.rls?.sessionVariable??"app.current_tenant";const result=await db.execute(sql`SELECT current_setting(${varName}, true) as tenant`);return result?.rows?.[0]?.tenant??null};export const createRLSPolicy=(tableName,columnName,policyName)=>{const config=getTenancyConfig();const varName=config.rls?.sessionVariable??"app.current_tenant";const column=columnName??config.rls?.defaultColumn??"tenant_id";const name=policyName??`${tableName}_tenant_isolation`;return pgPolicy(name,{for:"all",to:"public",using:sql`${sql.identifier(column)} = current_setting(${varName}, true)::text`,withCheck:sql`${sql.identifier(column)} = current_setting(${varName}, true)::text`})};export class DrizzleSchemaTenancy{mode="schema";resolveTable(table,tenant){const builders=getTableBuilders(table);if(!builders){throw new Error(`No column builders registered for table. `+`Use pgTable() from '@elysiumjs/mnemosyne-drizzle' for tenant support.`)}const config=getTableConfig(table);return wrapTenantSchema(tenant,config.name,builders)}async withIsolation(_connection,_tenant,callback){return callback()}}export class DrizzleRLSTenancy{mode="rls";resolveTable(table,_tenant){return table}async withIsolation(connection,tenant,callback){const config=getTenancyConfig();const varName=config.rls?.sessionVariable??"app.current_tenant";return connection.transaction(async(tx)=>{await tx.execute(sql`SELECT set_config(${varName}, ${tenant}, true)`);return callback()})}}
|
package/scripts/minify.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Glob } from 'bun';
|
|
2
|
-
|
|
3
|
-
const glob = new Glob('dist/**/*.js');
|
|
4
|
-
|
|
5
|
-
for await (const path of glob.scan('.')) {
|
|
6
|
-
const file = Bun.file(path);
|
|
7
|
-
const code = await file.text();
|
|
8
|
-
const result = new Bun.Transpiler({ minifyWhitespace: true, trimUnusedImports: true }).transformSync(code);
|
|
9
|
-
await Bun.write(file, result);
|
|
10
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { existsSync } from 'node:fs';
|
|
16
|
-
import { exists } from 'node:fs/promises';
|
|
17
|
-
import { dirname, join } from 'node:path';
|
|
18
|
-
|
|
19
|
-
import { Command, CommandArgumentType } from '@elysiumjs/core';
|
|
20
|
-
import prompts from 'prompts';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Gets the root path of the Elysium.js project: the closest parent holding an `.elysiumrc`.
|
|
24
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
25
|
-
*/
|
|
26
|
-
const getProjectPath = () => {
|
|
27
|
-
const startPath = process.cwd();
|
|
28
|
-
let dir = startPath;
|
|
29
|
-
|
|
30
|
-
while (true) {
|
|
31
|
-
if (existsSync(join(dir, '.elysiumrc'))) return dir;
|
|
32
|
-
|
|
33
|
-
const parentDir = dirname(dir);
|
|
34
|
-
if (dir === parentDir) break;
|
|
35
|
-
|
|
36
|
-
dir = parentDir;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return startPath;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Generates migration files.
|
|
44
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
45
|
-
*/
|
|
46
|
-
@Command.register()
|
|
47
|
-
export class MigrationGenerateCommand extends Command {
|
|
48
|
-
public static readonly command: string = 'migration:generate';
|
|
49
|
-
public static readonly description: string = 'Generates migration files.';
|
|
50
|
-
|
|
51
|
-
@Command.arg({
|
|
52
|
-
description: 'The name of the migration to create',
|
|
53
|
-
type: CommandArgumentType.STRING
|
|
54
|
-
})
|
|
55
|
-
private name?: string;
|
|
56
|
-
|
|
57
|
-
public async run(): Promise<void> {
|
|
58
|
-
if (!this.name) {
|
|
59
|
-
const answers = await prompts(
|
|
60
|
-
[
|
|
61
|
-
{
|
|
62
|
-
type: 'text',
|
|
63
|
-
name: 'name',
|
|
64
|
-
message: 'Migration Name:',
|
|
65
|
-
initial: 'create_users_table',
|
|
66
|
-
validate(value: string) {
|
|
67
|
-
if (value.length < 1) {
|
|
68
|
-
return 'Migration name cannot be empty';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
],
|
|
75
|
-
{
|
|
76
|
-
onCancel: () => {
|
|
77
|
-
this.error('Operation cancelled.');
|
|
78
|
-
process.exit(0);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
this.name = answers.name;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const projectRoot = getProjectPath();
|
|
87
|
-
let schemaPath = `${projectRoot}/src/database/schemas`;
|
|
88
|
-
if (await exists(`${projectRoot}/src/database/schema.ts`)) {
|
|
89
|
-
schemaPath = `${projectRoot}/src/database/schema.ts`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
await Bun.$`bunx --bun drizzle-kit generate --dialect postgresql --schema ${schemaPath} --out ${projectRoot}/src/database/migrations --name ${this.name}`;
|
|
93
|
-
|
|
94
|
-
const sqlFiles = new Bun.Glob(`${projectRoot}/src/database/migrations/**/*.sql`).scan();
|
|
95
|
-
for await (const path of sqlFiles) {
|
|
96
|
-
const file = Bun.file(path);
|
|
97
|
-
const content = await file.text();
|
|
98
|
-
await file.write(content.replace(/"public"\./gm, ''));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { Command, CommandArgumentType } from '@elysiumjs/core';
|
|
16
|
-
import prompts from 'prompts';
|
|
17
|
-
|
|
18
|
-
import { Database } from '../database';
|
|
19
|
-
import { getMigrationName, planMigrations } from '../migrations';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Execute migration files.
|
|
23
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
24
|
-
*/
|
|
25
|
-
@Command.register()
|
|
26
|
-
export class MigrationRunCommand extends Command {
|
|
27
|
-
public static readonly command: string = 'migration:run';
|
|
28
|
-
public static readonly description: string = 'Runs pending migrations.';
|
|
29
|
-
|
|
30
|
-
@Command.arg({
|
|
31
|
-
description: 'The name of the tenant in which the migrations will be executed.',
|
|
32
|
-
type: CommandArgumentType.STRING,
|
|
33
|
-
required: true
|
|
34
|
-
})
|
|
35
|
-
private tenant: string = '';
|
|
36
|
-
|
|
37
|
-
@Command.arg({
|
|
38
|
-
description: 'The name of the connection to use.',
|
|
39
|
-
type: CommandArgumentType.STRING,
|
|
40
|
-
default: 'default'
|
|
41
|
-
})
|
|
42
|
-
private connection: string = 'default';
|
|
43
|
-
|
|
44
|
-
@Command.arg({
|
|
45
|
-
description: 'Drop the entire schema before running the migrations.',
|
|
46
|
-
type: CommandArgumentType.BOOLEAN,
|
|
47
|
-
default: false
|
|
48
|
-
})
|
|
49
|
-
private fresh: boolean = false;
|
|
50
|
-
|
|
51
|
-
public async run(): Promise<void> {
|
|
52
|
-
if (!Database.connectionExists(this.connection)) {
|
|
53
|
-
this.error(`Connection ${this.bold(this.connection)} does not exist.`);
|
|
54
|
-
return process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const sql = Database.getConnection(this.connection).$client;
|
|
58
|
-
await sql.connect();
|
|
59
|
-
|
|
60
|
-
await sql.begin(async (sql: Bun.SQL) => {
|
|
61
|
-
if (this.fresh) {
|
|
62
|
-
await sql`DROP SCHEMA IF EXISTS ${sql(this.tenant)} CASCADE;`;
|
|
63
|
-
await sql`CREATE SCHEMA ${sql(this.tenant)};`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const [{ schema_exists }] = await sql`SELECT EXISTS (
|
|
67
|
-
SELECT schema_name
|
|
68
|
-
FROM information_schema.schemata
|
|
69
|
-
WHERE schema_name = ${this.tenant}
|
|
70
|
-
) AS schema_exists;`;
|
|
71
|
-
|
|
72
|
-
if (!schema_exists) {
|
|
73
|
-
const answers = await prompts(
|
|
74
|
-
[
|
|
75
|
-
{
|
|
76
|
-
type: 'confirm',
|
|
77
|
-
name: 'create',
|
|
78
|
-
message: `Schema ${this.tenant} does not exist. Do you want to create it?`,
|
|
79
|
-
initial: true
|
|
80
|
-
}
|
|
81
|
-
],
|
|
82
|
-
{
|
|
83
|
-
onCancel: () => {
|
|
84
|
-
this.error('Operation cancelled.');
|
|
85
|
-
process.exit(0);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (answers.create) {
|
|
91
|
-
await sql`CREATE SCHEMA ${sql(this.tenant)};`;
|
|
92
|
-
} else {
|
|
93
|
-
this.error(`Schema ${this.bold(this.tenant)} creation cancelled.`);
|
|
94
|
-
return process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
await sql`SET search_path TO ${sql(this.tenant)};`;
|
|
99
|
-
|
|
100
|
-
const [{ table_exists }] = await sql`SELECT EXISTS (
|
|
101
|
-
SELECT *
|
|
102
|
-
FROM information_schema.tables
|
|
103
|
-
WHERE table_name = 'elysium_migrations'
|
|
104
|
-
AND table_schema = ${this.tenant}
|
|
105
|
-
) AS table_exists;`;
|
|
106
|
-
|
|
107
|
-
if (!table_exists) {
|
|
108
|
-
await sql`CREATE TABLE elysium_migrations (
|
|
109
|
-
id SERIAL PRIMARY KEY,
|
|
110
|
-
name TEXT NOT NULL,
|
|
111
|
-
batch INTEGER NOT NULL,
|
|
112
|
-
applied_at TIMESTAMP NOT NULL
|
|
113
|
-
);`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const rows: { name: string; applied_at: Date }[] =
|
|
117
|
-
await sql`SELECT name, applied_at FROM elysium_migrations;`;
|
|
118
|
-
const completedMigrations = rows.map((row) => ({
|
|
119
|
-
name: row.name,
|
|
120
|
-
appliedAt: row.applied_at
|
|
121
|
-
}));
|
|
122
|
-
|
|
123
|
-
const filesIt = new Bun.Glob(`./src/database/migrations/**/*.sql`).scan({
|
|
124
|
-
absolute: true,
|
|
125
|
-
onlyFiles: true
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const files = await Array.fromAsync(filesIt);
|
|
129
|
-
const { pending: sqlFiles, renames } = planMigrations(files, completedMigrations);
|
|
130
|
-
|
|
131
|
-
for (const { from, to } of renames) {
|
|
132
|
-
await sql`UPDATE elysium_migrations SET name = ${to} WHERE name = ${from};`;
|
|
133
|
-
this.info(`Migration ${this.bold(from)} is now tracked as ${this.bold(to)}.`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (sqlFiles.length === 0) {
|
|
137
|
-
this.success('No new migrations to apply.');
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const s = this.spinner(`Running migrations for tenant ${this.bold(this.tenant)}...`);
|
|
142
|
-
|
|
143
|
-
const [{ max }]: [{ max?: number }] =
|
|
144
|
-
await sql`SELECT MAX(batch) + 1 AS max FROM elysium_migrations;`;
|
|
145
|
-
|
|
146
|
-
const batch = max ?? 0;
|
|
147
|
-
|
|
148
|
-
for (const path of sqlFiles) {
|
|
149
|
-
await sql.file(path);
|
|
150
|
-
const [{ name }] =
|
|
151
|
-
await sql`INSERT INTO elysium_migrations (name, batch, applied_at) VALUES (${getMigrationName(path)}, ${batch}, ${new Date()}) RETURNING name;`;
|
|
152
|
-
s.pause(`Migration ${this.bold(name)} applied successfully.`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
s.complete(`Migrations for tenant ${this.bold(this.tenant)} applied successfully.`);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await sql.close();
|
|
159
|
-
}
|
|
160
|
-
}
|
package/src/database.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { CacheInterface, DatabaseCacheConfig, DatabaseDriver } from '@elysiumjs/mnemosyne';
|
|
16
|
-
import type { DrizzleConfig } from 'drizzle-orm';
|
|
17
|
-
import type { BunSQLDatabase } from 'drizzle-orm/bun-sql/postgres';
|
|
18
|
-
import type { CacheConfig } from 'drizzle-orm/cache/core/types';
|
|
19
|
-
|
|
20
|
-
import { Service, ServiceNotReadyError } from '@elysiumjs/core';
|
|
21
|
-
import { AbstractDatabase, createDatabaseCacheStorage } from '@elysiumjs/mnemosyne';
|
|
22
|
-
import { getTableName, is, Table } from 'drizzle-orm';
|
|
23
|
-
import { drizzle } from 'drizzle-orm/bun-sql';
|
|
24
|
-
import { Cache as DrizzleCache } from 'drizzle-orm/cache/core';
|
|
25
|
-
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// Types
|
|
28
|
-
// ============================================================================
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Connection configuration for Drizzle ORM.
|
|
32
|
-
*
|
|
33
|
-
* Extends `DrizzleConfig` with connection details and optional caching.
|
|
34
|
-
*
|
|
35
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
36
|
-
* @template TSchema The Drizzle schema type.
|
|
37
|
-
*/
|
|
38
|
-
export type DrizzleConnectionProps<
|
|
39
|
-
TSchema extends Record<string, unknown> = Record<string, never>
|
|
40
|
-
> = Omit<DrizzleConfig<TSchema>, 'cache'> &
|
|
41
|
-
({ connection: string | ({ url?: string } & Bun.SQLOptions) } | { client: Bun.SQL }) & {
|
|
42
|
-
cache?: true | DatabaseCacheConfig;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* A Drizzle database connection backed by Bun's SQL driver.
|
|
47
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
48
|
-
*/
|
|
49
|
-
export type DrizzleConnection = BunSQLDatabase<Record<string, never>> & { $client: Bun.SQL };
|
|
50
|
-
|
|
51
|
-
// ============================================================================
|
|
52
|
-
// DrizzleDatabaseCache
|
|
53
|
-
// ============================================================================
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Drizzle query cache backed by the framework's cache storage.
|
|
57
|
-
*
|
|
58
|
-
* Extends the Drizzle `Cache` base class and implements the
|
|
59
|
-
* framework's `DatabaseCacheStrategy` interface so that query
|
|
60
|
-
* results can be transparently cached and invalidated.
|
|
61
|
-
*
|
|
62
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
63
|
-
*/
|
|
64
|
-
export class DrizzleDatabaseCache extends DrizzleCache {
|
|
65
|
-
#storageEngine: 'redis' | 'memory';
|
|
66
|
-
#cache?: Omit<CacheInterface, 'tags'>;
|
|
67
|
-
private usedTablesPerKey: Record<string, string[]> = {};
|
|
68
|
-
|
|
69
|
-
constructor(
|
|
70
|
-
private readonly cacheStrategy: 'explicit' | 'all' = 'explicit',
|
|
71
|
-
private readonly globalTtl: number = 1,
|
|
72
|
-
storageEngine: 'redis' | 'memory' = 'redis'
|
|
73
|
-
) {
|
|
74
|
-
super();
|
|
75
|
-
this.#storageEngine = storageEngine;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Resolves the cache storage on first use rather than at construction, so that a connection
|
|
80
|
-
* can be built before the module providing the cache has finished initialising.
|
|
81
|
-
*/
|
|
82
|
-
async #storage(): Promise<Omit<CacheInterface, 'tags'>> {
|
|
83
|
-
if (this.#cache) return this.#cache;
|
|
84
|
-
try {
|
|
85
|
-
this.#cache = createDatabaseCacheStorage({ storage: this.#storageEngine });
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (!(error instanceof ServiceNotReadyError)) throw error;
|
|
88
|
-
await Service.resolve(error.service);
|
|
89
|
-
this.#cache = createDatabaseCacheStorage({ storage: this.#storageEngine });
|
|
90
|
-
}
|
|
91
|
-
return this.#cache;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
public override strategy(): 'explicit' | 'all' {
|
|
95
|
-
return this.cacheStrategy;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
public override async get(key: string): Promise<any[] | undefined> {
|
|
99
|
-
return (await (await this.#storage()).get<any[]>(key)) ?? undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
public override async put(
|
|
103
|
-
key: string,
|
|
104
|
-
response: any,
|
|
105
|
-
tables: string[],
|
|
106
|
-
_isTag: boolean,
|
|
107
|
-
config?: CacheConfig
|
|
108
|
-
): Promise<void> {
|
|
109
|
-
await (await this.#storage()).set(key, response, (config?.ex ?? this.globalTtl) * 1000);
|
|
110
|
-
for (const table of tables) {
|
|
111
|
-
const keys = this.usedTablesPerKey[table];
|
|
112
|
-
if (keys === undefined) {
|
|
113
|
-
this.usedTablesPerKey[table] = [key];
|
|
114
|
-
} else {
|
|
115
|
-
keys.push(key);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
public override async onMutate(params: {
|
|
121
|
-
tags?: string | string[];
|
|
122
|
-
tables?: string | string[] | Table<any> | Table<any>[];
|
|
123
|
-
}): Promise<void> {
|
|
124
|
-
const tagsArray = params.tags ? (Array.isArray(params.tags) ? params.tags : [params.tags]) : [];
|
|
125
|
-
const tablesArray = params.tables
|
|
126
|
-
? Array.isArray(params.tables)
|
|
127
|
-
? params.tables
|
|
128
|
-
: [params.tables]
|
|
129
|
-
: [];
|
|
130
|
-
const keysToDelete = new Set<string>();
|
|
131
|
-
|
|
132
|
-
for (const table of tablesArray) {
|
|
133
|
-
const tableName = is(table, Table) ? getTableName(table as Table) : (table as string);
|
|
134
|
-
const keys = this.usedTablesPerKey[tableName] ?? [];
|
|
135
|
-
for (const key of keys) keysToDelete.add(key);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (keysToDelete.size > 0 || tagsArray.length > 0) {
|
|
139
|
-
const cache = await this.#storage();
|
|
140
|
-
await cache.mdel(tagsArray);
|
|
141
|
-
await cache.mdel([...keysToDelete]);
|
|
142
|
-
for (const table of tablesArray) {
|
|
143
|
-
const tableName = is(table, Table) ? getTableName(table as Table) : (table as string);
|
|
144
|
-
this.usedTablesPerKey[tableName] = [];
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ============================================================================
|
|
151
|
-
// DrizzleDatabase
|
|
152
|
-
// ============================================================================
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Drizzle-specific database manager.
|
|
156
|
-
*
|
|
157
|
-
* Implements the abstract database class with a driver that creates
|
|
158
|
-
* Drizzle connections using Bun's built-in SQL client.
|
|
159
|
-
*
|
|
160
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
161
|
-
*/
|
|
162
|
-
class DrizzleDatabase extends AbstractDatabase<DrizzleConnection, DrizzleConnectionProps> {
|
|
163
|
-
/**
|
|
164
|
-
* Creates a Drizzle connection from its properties. Used by the `DrizzleModule` to build
|
|
165
|
-
* the `db.connection.{name}` services.
|
|
166
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
167
|
-
* @param props The connection properties.
|
|
168
|
-
* @returns A new connection.
|
|
169
|
-
*/
|
|
170
|
-
public createConnection(props: DrizzleConnectionProps): DrizzleConnection {
|
|
171
|
-
return this.driver.createConnection(props);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
protected driver: DatabaseDriver<DrizzleConnection, DrizzleConnectionProps> = {
|
|
175
|
-
createConnection(config) {
|
|
176
|
-
const cache =
|
|
177
|
-
config.cache === undefined
|
|
178
|
-
? undefined
|
|
179
|
-
: config.cache === true
|
|
180
|
-
? new DrizzleDatabaseCache()
|
|
181
|
-
: config.cache.enabled !== false
|
|
182
|
-
? new DrizzleDatabaseCache(
|
|
183
|
-
config.cache.strategy,
|
|
184
|
-
config.cache.ttl,
|
|
185
|
-
config.cache.storage
|
|
186
|
-
)
|
|
187
|
-
: undefined;
|
|
188
|
-
return drizzle({ ...config, cache }) as DrizzleConnection;
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
withTransaction(connection, callback) {
|
|
192
|
-
return (connection as any).transaction((tx: any) => callback(tx));
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
getRawClient(connection) {
|
|
196
|
-
return connection.$client;
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Singleton Drizzle database manager.
|
|
203
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
204
|
-
*/
|
|
205
|
-
export const Database = new DrizzleDatabase();
|
package/src/drizzle.module.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { TenancyConfig } from '@elysiumjs/mnemosyne';
|
|
16
|
-
import type { DrizzleConnection, DrizzleConnectionProps } from './database';
|
|
17
|
-
|
|
18
|
-
import { Module, needsDefaultAlias, runAll, Service } from '@elysiumjs/core';
|
|
19
|
-
import {
|
|
20
|
-
AbstractDatabase,
|
|
21
|
-
MnemosyneModule,
|
|
22
|
-
TENANCY_CONFIG_SERVICE,
|
|
23
|
-
TENANCY_STRATEGY_SERVICE
|
|
24
|
-
} from '@elysiumjs/mnemosyne';
|
|
25
|
-
|
|
26
|
-
import { MigrationGenerateCommand } from './commands/migration_generate.command';
|
|
27
|
-
import { MigrationRunCommand } from './commands/migration_run.command';
|
|
28
|
-
import { Database } from './database';
|
|
29
|
-
import { DrizzleRLSTenancy, DrizzleSchemaTenancy } from './tenancy';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Configuration of the {@link DrizzleModule}.
|
|
33
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
34
|
-
*/
|
|
35
|
-
export type DrizzleModuleConfig = {
|
|
36
|
-
/** The name of the connection that `default` resolves to. */
|
|
37
|
-
default: string;
|
|
38
|
-
|
|
39
|
-
/** The connections to declare, by name. */
|
|
40
|
-
connections: Record<string, DrizzleConnectionProps>;
|
|
41
|
-
|
|
42
|
-
/** The tenancy configuration. Defaults to schema-based tenancy. */
|
|
43
|
-
tenancy?: TenancyConfig;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Declares Drizzle connections as global services named `db.connection.{name}`, plus
|
|
48
|
-
* `db.connection.default` aliasing the configured default, and registers the tenancy config
|
|
49
|
-
* and strategy. Connections are created on first use. Also provides the migration commands.
|
|
50
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
51
|
-
*/
|
|
52
|
-
@Module.register<DrizzleModuleConfig>({
|
|
53
|
-
imports: [MnemosyneModule],
|
|
54
|
-
commands: [MigrationGenerateCommand, MigrationRunCommand],
|
|
55
|
-
services: (config) => {
|
|
56
|
-
const alias = needsDefaultAlias('DrizzleModule', config);
|
|
57
|
-
const tenancy = config.tenancy ?? { mode: 'schema' };
|
|
58
|
-
return [
|
|
59
|
-
...Object.entries(config.connections).map(([name, props]) => ({
|
|
60
|
-
name: AbstractDatabase.getConnectionName(name),
|
|
61
|
-
global: true,
|
|
62
|
-
factory: () => Database.createConnection(props)
|
|
63
|
-
})),
|
|
64
|
-
...(alias
|
|
65
|
-
? [
|
|
66
|
-
{
|
|
67
|
-
name: AbstractDatabase.getConnectionName('default'),
|
|
68
|
-
global: true,
|
|
69
|
-
factory: (_config: unknown, get: <T>(name: string) => T) =>
|
|
70
|
-
get<DrizzleConnection>(AbstractDatabase.getConnectionName(config.default))
|
|
71
|
-
}
|
|
72
|
-
]
|
|
73
|
-
: []),
|
|
74
|
-
{ name: TENANCY_CONFIG_SERVICE, global: true, factory: () => tenancy },
|
|
75
|
-
{
|
|
76
|
-
name: TENANCY_STRATEGY_SERVICE,
|
|
77
|
-
global: true,
|
|
78
|
-
factory: () =>
|
|
79
|
-
tenancy.mode === 'rls' ? new DrizzleRLSTenancy() : new DrizzleSchemaTenancy()
|
|
80
|
-
}
|
|
81
|
-
];
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
export class DrizzleModule extends Module.configurable<DrizzleModuleConfig>({
|
|
85
|
-
defaults: { default: 'default' },
|
|
86
|
-
required: ['connections']
|
|
87
|
-
}) {
|
|
88
|
-
/** Closes the clients of the connections that were created. */
|
|
89
|
-
public override async onDestroy(): Promise<void> {
|
|
90
|
-
const names = Object.keys(this.config.connections).map(AbstractDatabase.getConnectionName);
|
|
91
|
-
await runAll(
|
|
92
|
-
names
|
|
93
|
-
.filter((name) => Service.isConstructed(name))
|
|
94
|
-
.map((name) => () => Service.get<DrizzleConnection>(name)?.$client.close())
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|