@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/cache.d.ts +60 -0
  3. package/dist/commands/migration_run.command.d.ts +13 -0
  4. package/dist/commands/project.d.ts +11 -0
  5. package/dist/database.d.ts +82 -31
  6. package/dist/driver.d.ts +39 -0
  7. package/dist/drivers/bun-sql.d.ts +25 -0
  8. package/dist/drivers/bun-sql.js +1 -0
  9. package/dist/drivers/hyperdrive.d.ts +46 -0
  10. package/dist/drivers/hyperdrive.js +1 -0
  11. package/dist/drivers/node-postgres.d.ts +23 -0
  12. package/dist/drivers/node-postgres.js +1 -0
  13. package/dist/drivers/postgres-js.d.ts +25 -0
  14. package/dist/drivers/postgres-js.js +1 -0
  15. package/dist/drizzle.module.d.ts +3 -3
  16. package/dist/index.d.ts +5 -3
  17. package/dist/index.js +11 -1
  18. package/dist/migrations.d.ts +12 -0
  19. package/dist/model.d.ts +9 -9
  20. package/dist/query.d.ts +28 -0
  21. package/dist/repository.d.ts +34 -6
  22. package/dist/tenancy.d.ts +25 -8
  23. package/package.json +63 -11
  24. package/.omc/state/agent-replay-3920b579-6d8e-4f5f-924a-319202936685.jsonl +0 -7
  25. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/last-tool-error-state.json +0 -7
  26. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/mission-state.json +0 -93
  27. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/pre-tool-advisory-throttle.json +0 -18
  28. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/subagent-tracking-state.json +0 -61
  29. package/.turbo/turbo-build.log +0 -3
  30. package/dist/commands/migration_generate.command.js +0 -1
  31. package/dist/commands/migration_run.command.js +0 -15
  32. package/dist/database.js +0 -1
  33. package/dist/drizzle.module.js +0 -1
  34. package/dist/migrations.js +0 -1
  35. package/dist/model.js +0 -1
  36. package/dist/repository.js +0 -1
  37. package/dist/table.js +0 -1
  38. package/dist/tenancy.js +0 -1
  39. package/scripts/minify.ts +0 -10
  40. package/src/commands/migration_generate.command.ts +0 -101
  41. package/src/commands/migration_run.command.ts +0 -160
  42. package/src/database.ts +0 -205
  43. package/src/drizzle.module.ts +0 -97
  44. package/src/index.ts +0 -47
  45. package/src/migrations.ts +0 -100
  46. package/src/model.ts +0 -178
  47. package/src/repository.ts +0 -113
  48. package/src/table.ts +0 -70
  49. package/src/tenancy.ts +0 -249
  50. package/tests/drizzle.module.test.ts +0 -201
  51. package/tests/migrations.test.ts +0 -123
  52. package/tests/model.test.ts +0 -362
  53. package/tests/setup.ts +0 -0
  54. package/tests/table.test.ts +0 -72
  55. package/tests/tenancy.test.ts +0 -212
  56. package/tsconfig.json +0 -12
package/dist/index.js CHANGED
@@ -1 +1,11 @@
1
- export*from"drizzle-orm";export*from"drizzle-orm/pg-core";export{pgTable,getTableBuilders}from"./table";export{Database,DrizzleDatabaseCache}from"./database";export{DrizzleModel,createSchemaFromDrizzle,drizzleAdapter}from"./model";export{DrizzleModule}from"./drizzle.module";export{DrizzleRepository}from"./repository";export{DrizzleSchemaTenancy,DrizzleRLSTenancy,wrapTenantSchema,registerTenantSchema,getTenantSchema,withRLS,setConnectionTenant,getSessionTenant,createRLSPolicy}from"./tenancy";
1
+ var __legacyDecorateClassTS=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 __legacyMetadataTS=(k,v)=>{if(typeof Reflect==="object"&&typeof Reflect.metadata==="function")return Reflect.metadata(k,v)};export*from"drizzle-orm";export*from"drizzle-orm/pg-core";import{pgTable as drizzlePgTable}from"drizzle-orm/pg-core";var tableRegistry=new WeakMap,getTableBuilders=(table)=>tableRegistry.get(table),pgTable=(tableName,columns,extraConfig)=>{let table=drizzlePgTable(tableName,columns,extraConfig);return tableRegistry.set(table,columns),table};import{ElysiumError as ElysiumError2}from"@elysiumjs/core";import{AbstractDatabase,getCurrentTenant as getCurrentTenant2,getTenancyConfig}from"@elysiumjs/mnemosyne";import{sql}from"drizzle-orm";import{AsyncLocalStorage}from"node:async_hooks";import{Service,ServiceNotReadyError}from"@elysiumjs/core";import{createDatabaseCacheStorage,getCurrentTenant}from"@elysiumjs/mnemosyne";import{getTableName,is,Table}from"drizzle-orm";import{Cache as DrizzleCache}from"drizzle-orm/cache/core";var DEFAULT_CACHE_TTL=60,VERSION_TTL=86400000,MAX_PENDING_PER_KEY=16,MAX_PENDING_KEYS=1000,TABLE_SCHEMA=Symbol.for("drizzle:Schema"),tableKey=(table)=>{if(!is(table,Table))return table;let schema=table[TABLE_SCHEMA];return schema?`${schema}.${getTableName(table)}`:getTableName(table)},toArray=(value)=>value===void 0?[]:Array.isArray(value)?value:[value];class DrizzleDatabaseCache extends DrizzleCache{#storageEngine;#cache;#cacheStrategy;#globalTtl;#keyPrefix;#transactionScope=new AsyncLocalStorage;#pending=new Map;constructor(cacheStrategy="explicit",globalTtl=DEFAULT_CACHE_TTL,storageEngine="redis",connectionName="default",prefix=""){super();this.#cacheStrategy=cacheStrategy,this.#globalTtl=globalTtl,this.#storageEngine=storageEngine,this.#keyPrefix=prefix?`${prefix}:${connectionName}`:connectionName}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.token),this.#cache=createDatabaseCacheStorage({storage:this.#storageEngine})}return this.#cache}strategy(){return this.#cacheStrategy}async trackTransaction(run){let scope={tables:new Set,tags:new Set},result=await this.#transactionScope.run(scope,run);return await this.#invalidate([...scope.tables],[...scope.tags]),result}async get(key,tables=[],isTag=!1,autoInvalidate){if(this.#transactionScope.getStore())return;let storage=await this.#storage(),entryKey=this.#entryKey(key,isTag),versionKeys=autoInvalidate===!1?[]:tables.map((table)=>this.#versionKey(table)),[entry,...versions]=await storage.mget([entryKey,...versionKeys]),missing=versionKeys.flatMap((versionKey,index)=>versions[index]===void 0?[{index,versionKey}]:[]);if(missing.length>0){let created=missing.map(({versionKey})=>({key:versionKey,value:crypto.randomUUID(),ttl:VERSION_TTL}));await storage.mset(created);for(let[i,{index}]of missing.entries())versions[index]=created[i].value}let signature=versions.join("|"),cached=entry;if(cached!==void 0&&cached.v===signature)return cached.r;this.#remember(entryKey,{signature,versionKeys,at:Date.now()});return}async put(key,response,_tables,isTag,config){if(this.#transactionScope.getStore())return;let entryKey=this.#entryKey(key,isTag),pending=this.#live(entryKey);if(pending.length===0||new Set(pending.map((miss)=>miss.signature)).size>1){this.#pending.delete(entryKey);return}let[miss,...rest]=pending;if(rest.length>0)this.#pending.set(entryKey,rest);else this.#pending.delete(entryKey);let storage=await this.#storage();if((await storage.mget(miss.versionKeys)).join("|")!==miss.signature)return;let signature=miss.signature,ttl=await this.#ttl(storage,entryKey,config);if(ttl<=0)return;await storage.set(entryKey,{v:signature,r:response},ttl)}async onMutate(params){let tables=toArray(params.tables).map(tableKey),tags=toArray(params.tags),scope=this.#transactionScope.getStore();if(scope){for(let table of tables)scope.tables.add(table);for(let tag of tags)scope.tags.add(tag);return}await this.#invalidate(tables,tags)}#remember(entryKey,miss){let pending=[...this.#live(entryKey),miss].slice(-MAX_PENDING_PER_KEY);this.#pending.delete(entryKey),this.#pending.set(entryKey,pending);while(this.#pending.size>MAX_PENDING_KEYS)this.#pending.delete(this.#pending.keys().next().value)}#live(entryKey){let since=Date.now()-this.#globalTtl*1000;return(this.#pending.get(entryKey)??[]).filter((miss)=>miss.at>=since)}async#invalidate(tables,tags){if(tables.length===0&&tags.length===0)return;let storage=await this.#storage();if(tables.length>0)await storage.mset(tables.map((table)=>({key:this.#versionKey(table),value:crypto.randomUUID(),ttl:VERSION_TTL})));if(tags.length>0)await storage.mdel(tags.map((tag)=>this.#entryKey(tag,!0)))}async#ttl(storage,entryKey,config){if(config?.px!==void 0)return config.px;if(config?.ex!==void 0)return config.ex*1000;if(config?.pxat!==void 0)return config.pxat-Date.now();if(config?.exat!==void 0)return config.exat*1000-Date.now();if(config?.keepTtl){let expiresAt=await storage.ttl(entryKey);if(expiresAt!==void 0&&expiresAt>Date.now())return expiresAt-Date.now()}return this.#globalTtl*1000}#entryKey(key,isTag){let kind=isTag?"tag":"query";return`${this.#keyPrefix}:${kind}:${getCurrentTenant()??"public"}:${key}`}#versionKey(table){return`${this.#keyPrefix}:version:${table}`}}import{ElysiumError}from"@elysiumjs/core";class MissingDriverError extends ElysiumError{constructor(name){super(`Connection "${name}" sets no driver, and the default Bun SQL driver runs on Bun only. Set \`driver\` to postgresJs() or nodePostgres() on Node.js, or hyperdrive() on Cloudflare Workers.`)}}var connectionDrivers=new WeakMap,defaultDriver=(props,name)=>{if(typeof globalThis.Bun>"u")throw new MissingDriverError(name);let{drizzle}=import.meta.require("drizzle-orm/bun-sql"),client="client"in props?{client:props.client}:{connection:props.connection};return{name:"bun-sql",connect:(config)=>drizzle({...config,...client}),close:async(connection)=>{await connection.$client.close()}}},closeConnection=async(connection)=>{let driver=connectionDrivers.get(connection);if(driver)await driver.close(connection)};class TenantMismatchError extends ElysiumError2{transactionTenant;tenant;constructor(transactionTenant,tenant){super(`Cannot run work for tenant "${tenant}" in a transaction bound to `+(transactionTenant===null?"no tenant":`tenant "${transactionTenant}"`)+". Run it outside the transaction, or in its own withRLS() before opening the transaction.");this.transactionTenant=transactionTenant;this.tenant=tenant}}var transactionTenants=new WeakMap,bindTransactionTenant=async(tx,tenant)=>{let varName=getTenancyConfig().rls?.sessionVariable??"app.current_tenant";await tx.execute(sql`SELECT set_config(${varName}, ${tenant}, true)`),transactionTenants.set(tx,tenant)},assertTransactionTenant=(tx,tenant)=>{let bound=transactionTenants.get(tx);if(bound!==tenant)throw new TenantMismatchError(bound??null,tenant)};class DrizzleDatabase extends AbstractDatabase{getConnection(name="default"){let connection=super.getConnection(name);if(transactionTenants.has(connection)&&getTenancyConfig().mode==="rls")assertTransactionTenant(connection,getCurrentTenant2()??"public");return connection}async transaction(callback,name="default"){if(getTenancyConfig().mode==="rls"){let active=this.getActiveTransaction(this.getBaseConnection(name));if(active!==void 0)assertTransactionTenant(active,getCurrentTenant2()??"public")}return super.transaction(callback,name)}createConnection(props,name="default"){return this.driver.createConnection(props,name)}driver={createConnection(config,name="default"){let cache=config.cache===void 0?void 0:config.cache===!0?new DrizzleDatabaseCache(void 0,void 0,void 0,name):config.cache.enabled!==!1?new DrizzleDatabaseCache(config.cache.strategy,config.cache.ttl,config.cache.storage,name,config.cache.prefix):void 0,{driver:chosen,connection:_url,client:_client,cache:_cache,...drizzleConfig}=config,driver=chosen??defaultDriver(config,name),connection=driver.connect({...drizzleConfig,cache});if(connectionDrivers.set(connection,driver),cache===void 0)return connection;let transaction=(callback,txConfig)=>Object.getPrototypeOf(connection).transaction.call(connection,callback,txConfig);return Object.assign(connection,{transaction:(callback,txConfig)=>cache.trackTransaction(()=>transaction(callback,txConfig))})},withTransaction(connection,callback){return connection.transaction(async(tx)=>{if(getTenancyConfig().mode==="rls")await bindTransactionTenant(tx,getCurrentTenant2()??"public");return callback(tx)})},getRawClient(connection){return connection.$client}}}var Database=new DrizzleDatabase;import{AbstractModel,createSchemaFromModel}from"@elysiumjs/mnemosyne";import{getTableConfig as getTableConfig2}from"drizzle-orm/pg-core";import{getTenancyConfig as getTenancyConfig2,isValidTenantId,withTenant}from"@elysiumjs/mnemosyne";import{sql as sql2}from"drizzle-orm";import{getTableConfig,pgPolicy,pgSchema,pgTable as pgTable2}from"drizzle-orm/pg-core";var tenantSchemas=new Map,schemaRegistry=new Map,assertTenantId=(tenant)=>{if(!isValidTenantId(tenant))throw Error(`Invalid tenant identifier: ${JSON.stringify(tenant.slice(0,64))}`)},getTenantSchema=(tenant)=>(assertTenantId(tenant),schemaRegistry.get(tenant)??registerTenantSchema(tenant)),registerTenantSchema=(tenant)=>{if(assertTenantId(tenant),schemaRegistry.has(tenant))return schemaRegistry.get(tenant);let tenantSchema=pgSchema(tenant);return schemaRegistry.set(tenant,tenantSchema),tenantSchema},wrapTenantSchema=(tenant,tableName,columns)=>{if(assertTenantId(tenant),tenant==="public")return pgTable2(tableName,columns);let fullTableName=`${tenant}.${tableName}`;if(tenantSchemas.has(fullTableName))return tenantSchemas.get(fullTableName);let schemaTable=getTenantSchema(tenant).table(tableName,columns);return tenantSchemas.set(fullTableName,schemaTable),schemaTable},withRLS=async(tenant,callback,connectionName="default")=>new DrizzleRLSTenancy().withIsolation(Database.getBaseConnection(connectionName),tenant??"public",()=>callback(Database.getConnection(connectionName))),getSessionTenant=async(connectionName="default")=>{let db=Database.getConnection(connectionName),varName=getTenancyConfig2().rls?.sessionVariable??"app.current_tenant",result=await db.execute(sql2`SELECT current_setting(${varName}, true) as tenant`),[row]=Array.isArray(result)?result:result.rows;return row?.tenant||null},createRLSPolicy=(tableName,columnName,policyName)=>{let config=getTenancyConfig2(),varName=config.rls?.sessionVariable??"app.current_tenant",column=columnName??config.rls?.defaultColumn??"tenant_id",name=policyName??`${tableName}_tenant_isolation`;return pgPolicy(name,{for:"all",to:"public",using:sql2`${sql2.identifier(column)} = current_setting(${varName}, true)::text`,withCheck:sql2`${sql2.identifier(column)} = current_setting(${varName}, true)::text`})};class DrizzleSchemaTenancy{mode="schema";resolveTable(table,tenant){let builders=getTableBuilders(table);if(!builders)throw Error("No column builders registered for table. Use pgTable() from '@elysiumjs/mnemosyne-drizzle' for tenant support.");let config=getTableConfig(table);return wrapTenantSchema(tenant,config.name,builders)}async withIsolation(_connection,_tenant,callback){return callback()}}class DrizzleRLSTenancy{mode="rls";resolveTable(table,_tenant){return table}async withIsolation(connection,tenant,callback){let active=Database.getActiveTransaction(connection);if(active!==void 0)return assertTransactionTenant(active,tenant),withTenant(tenant,callback);return connection.transaction(async(tx)=>(await bindTransactionTenant(tx,tenant),withTenant(tenant,()=>Database.runInTransaction(connection,tx,callback))))}}var mapDrizzleDataType=(col)=>{if(col.columnType==="PgUUID")return"uuid";if(col.dimensions)return"array";let[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"}},drizzleAdapter={getTableName(table){return getTableConfig2(table).name},getColumns(table){let config=getTableConfig2(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){let builders=getTableBuilders(table);if(!builders)throw Error("No column builders registered for table. Use pgTable() from '@elysiumjs/mnemosyne-drizzle' for tenant support.");let config=getTableConfig2(table);return wrapTenantSchema(tenant,config.name,builders)}},createSchemaFromDrizzle=(table,opts)=>createSchemaFromModel(drizzleAdapter.getColumns(table),opts),DrizzleModel=(table,options)=>{let config=getTableConfig2(table),builders=getTableBuilders(table),Base=AbstractModel(config.name,builders??{},drizzleAdapter,table);if(options?.supportTenancy!==void 0)Base.supportTenancy=options.supportTenancy;return Base};import{Module,needsDefaultAlias,runAll,Service as Service2}from"@elysiumjs/core";import{AbstractDatabase as AbstractDatabase2,DATABASE_TRANSACTION_SERVICE,MnemosyneModule,TENANCY_CONFIG_SERVICE,TENANCY_STRATEGY_SERVICE,withUnitOfWork}from"@elysiumjs/mnemosyne";import{access}from"node:fs/promises";import{Command,CommandArgumentType}from"@elysiumjs/core";var basename=(path)=>path.split(/[\\/]/).pop()??path,dirname=(path)=>path.replace(/[\\/][^\\/]*$/,"");var getMigrationName=(path)=>basename(path)==="migration.sql"?basename(dirname(path)):basename(path),descriptiveName=(name)=>name.replace(/\.sql$/,"").split("_").slice(1).join("_"),folderTime=(name)=>{let 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]):void 0},planMigrations=(files,applied)=>{let appliedNames=new Set(applied.map(({name})=>name)),fileNames=new Set(files.map(getMigrationName)),moved=new Map;for(let record of[...applied].sort((a,b)=>a.name.localeCompare(b.name))){if(!record.name.endsWith(".sql")||fileNames.has(record.name))continue;let key=descriptiveName(record.name);moved.set(key,[...moved.get(key)??[],record])}let plan={pending:[],renames:[]};for(let file of[...files].sort()){let name=getMigrationName(file);if(appliedNames.has(name))continue;let from=basename(file)==="migration.sql"?takeMoved(moved,name):void 0;if(from)plan.renames.push({from,to:name});else plan.pending.push(file)}return plan},takeMoved=(moved,name)=>{let records=moved.get(descriptiveName(name)),generatedAt=folderTime(name),index=records?.findIndex(({appliedAt})=>generatedAt===void 0||generatedAt<=appliedAt.getTime())??-1;return index<0?void 0:records.splice(index,1)[0].name},IDENTIFIER_CHAR=/[A-Za-z0-9_$]/;var endOfQuoted=(sql,start,quote,backslash)=>{let i=start+1;while(i<sql.length)if(backslash&&sql[i]==="\\")i+=2;else if(sql[i]!==quote)i++;else if(sql[i+1]===quote)i+=2;else return i+1;return sql.length},endOfVerbatim=(sql,i)=>{let previous=sql[i-1]??"";if(sql.startsWith("--",i)){let end=sql.indexOf(`
2
+ `,i);return end<0?sql.length:end}if(sql.startsWith("/*",i)){let end=sql.indexOf("*/",i+2);return end<0?sql.length:end+2}if(sql[i]==="'"){let escape=/[Ee]/.test(previous)&&!IDENTIFIER_CHAR.test(sql[i-2]??"");return endOfQuoted(sql,i,"'",escape)}if(sql[i]==="$"&&!IDENTIFIER_CHAR.test(previous)){let tag=/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql.slice(i,i+64))?.[0];if(tag){let end=sql.indexOf(tag,i+tag.length);return end<0?sql.length:end+tag.length}}return-1},stripPublicSchema=(sql)=>{let output="",i=0;while(i<sql.length){let verbatim=endOfVerbatim(sql,i);if(verbatim>=0)output+=sql.slice(i,verbatim),i=verbatim;else if(sql.startsWith('"public".',i)&&sql[i+9]==='"')i+=9;else if(sql[i]==='"'){let end=endOfQuoted(sql,i,'"',!1);output+=sql.slice(i,end),i=end}else output+=sql[i],i++}return output};import{existsSync}from"node:fs";import{dirname as dirname2,join}from"node:path";var getProjectPath=()=>{let startPath=process.cwd(),dir=startPath;while(!0){if(existsSync(join(dir,".elysiumrc")))return dir;let parentDir=dirname2(dir);if(dir===parentDir)break;dir=parentDir}return startPath},getMigrationsPath=()=>join(getProjectPath(),"src/database/migrations");var exists=(path)=>access(path).then(()=>!0,()=>!1),listSqlFiles=async(migrationsPath)=>{if(!await exists(migrationsPath))return[];return Array.fromAsync(new Bun.Glob("**/*.sql").scan({cwd:migrationsPath,absolute:!0,onlyFiles:!0}))};class MigrationGenerateCommand extends Command{static command="migration:generate";static description="Generates migration files.";async run(){if(!this.name){let{default:prompts}=await import("prompts"),answers=await prompts([{type:"text",name:"name",message:"Migration Name:",initial:"create_users_table",validate(value){if(value.length<1)return"Migration name cannot be empty";return!0}}],{onCancel:()=>{this.error("Operation cancelled."),process.exit(0)}});this.name=answers.name}let projectRoot=getProjectPath(),migrationsPath=getMigrationsPath(),schemaPath=`${projectRoot}/src/database/schemas`;if(await exists(`${projectRoot}/src/database/schema.ts`))schemaPath=`${projectRoot}/src/database/schema.ts`;let existing=new Set(await listSqlFiles(migrationsPath));await Bun.$`bunx --bun drizzle-kit generate --dialect postgresql --schema ${schemaPath} --out ${migrationsPath} --name ${this.name}`;for(let path of await listSqlFiles(migrationsPath)){if(existing.has(path))continue;let file=Bun.file(path);await file.write(stripPublicSchema(await file.text()))}}}__legacyDecorateClassTS([Command.arg({description:"The name of the migration to create",type:CommandArgumentType.STRING}),__legacyMetadataTS("design:type",String)],MigrationGenerateCommand.prototype,"name",void 0),MigrationGenerateCommand=__legacyDecorateClassTS([Command.register()],MigrationGenerateCommand);import{readdir}from"node:fs/promises";import{join as join2}from"node:path";import{Command as Command2,CommandArgumentType as CommandArgumentType2}from"@elysiumjs/core";import{isValidTenantId as isValidTenantId2}from"@elysiumjs/mnemosyne";var listMigrationFiles=async(folder)=>(await readdir(folder,{recursive:!0,withFileTypes:!0}).catch((error)=>{if(error.code==="ENOENT")return[];throw error})).filter((entry)=>entry.isFile()&&entry.name.endsWith(".sql")).map((entry)=>join2(entry.parentPath,entry.name));class MigrationRunCommand extends Command2{constructor(){super(...arguments);this.tenant="";this.connection="default";this.fresh=!1;this.createSchema=!1}static command="migration:run";static description="Runs pending migrations.";async run(){if(!isValidTenantId2(this.tenant))return this.error(`Invalid tenant ${this.bold(this.tenant)}: use 1 to 63 letters, digits, underscores or dashes, not starting with pg_ and other than information_schema.`),process.exit(1);if(!Database.connectionExists(this.connection))return this.error(`Connection ${this.bold(this.connection)} does not exist.`),process.exit(1);let sql=Database.getConnection(this.connection).$client;await sql.connect();try{if(!this.fresh&&!await this.ensureSchemaChoice(sql))return process.exit(1);await this.migrate(sql)}finally{await sql.close()}}async migrate(sql){await sql.begin(async(sql)=>{if(await sql`SELECT pg_advisory_xact_lock(hashtext(${`elysium_migrations:${this.tenant}`}))`,this.fresh)await sql`DROP SCHEMA IF EXISTS ${sql(this.tenant)} CASCADE;`;await sql`CREATE SCHEMA IF NOT EXISTS ${sql(this.tenant)};`,await sql`SET LOCAL search_path TO ${sql(this.tenant)}`,await sql`CREATE TABLE IF NOT EXISTS elysium_migrations (
3
+ id SERIAL PRIMARY KEY,
4
+ name TEXT NOT NULL,
5
+ batch INTEGER NOT NULL,
6
+ applied_at TIMESTAMP NOT NULL
7
+ );`,await sql`CREATE UNIQUE INDEX IF NOT EXISTS elysium_migrations_name_unique ON elysium_migrations (name);`;let completedMigrations=(await sql`SELECT name, applied_at FROM elysium_migrations;`).map((row)=>({name:row.name,appliedAt:row.applied_at})),files=await listMigrationFiles(getMigrationsPath()),{pending:sqlFiles,renames}=planMigrations(files,completedMigrations);for(let{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}let s=this.spinner(`Running migrations for tenant ${this.bold(this.tenant)}...`),[{max}]=await sql`SELECT MAX(batch) + 1 AS max FROM elysium_migrations;`,batch=max??0;for(let path of sqlFiles){await sql.file(path);let[{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.`)})}async ensureSchemaChoice(sql){let[{schema_exists}]=await sql`SELECT EXISTS (
8
+ SELECT schema_name
9
+ FROM information_schema.schemata
10
+ WHERE schema_name = ${this.tenant}
11
+ ) AS schema_exists;`;if(schema_exists||this.createSchema)return!0;if(!process.stdin.isTTY)return this.error(`Schema ${this.bold(this.tenant)} does not exist. Run again with --create-schema to create it.`),!1;let{default:prompts}=await import("prompts");if(!(await prompts([{type:"confirm",name:"create",message:`Schema ${this.tenant} does not exist. Do you want to create it?`,initial:!0}],{onCancel:()=>{this.error("Operation cancelled."),process.exit(0)}})).create)return this.error(`Schema ${this.bold(this.tenant)} creation cancelled.`),!1;return!0}}__legacyDecorateClassTS([Command2.arg({description:"The name of the tenant in which the migrations will be executed.",type:CommandArgumentType2.STRING,required:!0}),__legacyMetadataTS("design:type",String)],MigrationRunCommand.prototype,"tenant",void 0),__legacyDecorateClassTS([Command2.arg({description:"The name of the connection to use.",type:CommandArgumentType2.STRING,default:"default"}),__legacyMetadataTS("design:type",String)],MigrationRunCommand.prototype,"connection",void 0),__legacyDecorateClassTS([Command2.arg({description:"Drop the entire schema before running the migrations.",type:CommandArgumentType2.BOOLEAN,default:!1}),__legacyMetadataTS("design:type",Boolean)],MigrationRunCommand.prototype,"fresh",void 0),__legacyDecorateClassTS([Command2.arg({name:"create-schema",description:"Create the tenant schema without asking when it does not exist.",type:CommandArgumentType2.BOOLEAN,default:!1}),__legacyMetadataTS("design:type",Boolean)],MigrationRunCommand.prototype,"createSchema",void 0),MigrationRunCommand=__legacyDecorateClassTS([Command2.register()],MigrationRunCommand);class DrizzleModule extends Module.configurable({defaults:{default:"default"},required:["connections"]}){async onDestroy(){let names=Object.keys(this.config.connections).map(AbstractDatabase2.getConnectionName);await runAll(names.filter((name)=>Service2.isConstructed(name)).map((name)=>()=>closeConnection(Service2.get(name))))}}DrizzleModule=__legacyDecorateClassTS([Module.register({imports:[MnemosyneModule],commands:[MigrationGenerateCommand,MigrationRunCommand],services:(config)=>{let alias=needsDefaultAlias("DrizzleModule",config),tenancy=config.tenancy??{mode:"schema"};return[...Object.entries(config.connections).map(([name,props])=>({name:AbstractDatabase2.getConnectionName(name),global:!0,factory:()=>Database.createConnection(props,name)})),...alias?[{name:AbstractDatabase2.getConnectionName("default"),global:!0,factory:(_config,get)=>get(AbstractDatabase2.getConnectionName(config.default))}]:[],{name:DATABASE_TRANSACTION_SERVICE,global:!0,factory:()=>withUnitOfWork((callback,connection)=>Database.transaction(()=>callback(),connection))},{name:TENANCY_CONFIG_SERVICE,global:!0,factory:()=>tenancy},{name:TENANCY_STRATEGY_SERVICE,global:!0,factory:()=>tenancy.mode==="rls"?new DrizzleRLSTenancy:new DrizzleSchemaTenancy}]}})],DrizzleModule);import{and,asc,desc,eq,gt,gte,ilike,inArray,isNotNull,isNull,lt,lte,ne,or}from"drizzle-orm";var escapeLike=(value)=>value.replace(/[\\%_]/g,(char)=>`\\${char}`),columnOf=(table,column)=>{let found=table[column];if(!found)throw Error(`The table has no column "${column}" for the query.`);return found},compileFilter=(table,filter)=>{let column=columnOf(table,filter.column),value=filter.value;switch(filter.operator){case"eq":return eq(column,value);case"ne":return ne(column,value);case"gt":return gt(column,value);case"gte":return gte(column,value);case"lt":return lt(column,value);case"lte":return lte(column,value);case"in":return inArray(column,value);case"contains":return ilike(column,`%${escapeLike(String(value))}%`);case"startsWith":return ilike(column,`${escapeLike(String(value))}%`);case"isNull":return value?isNull(column):isNotNull(column)}},compileAfter=(table,query)=>{if(!query.after)return;let branches=query.sort.map((key,index)=>{let equal=query.sort.slice(0,index).map((previous,i)=>eq(columnOf(table,previous.column),query.after[i])),column=columnOf(table,key.column),value=query.after[index];return and(...equal,key.direction==="asc"?gt(column,value):lt(column,value))});return or(...branches)},compileQuery=(table,query)=>({where:and(...query.filters.map((filter)=>compileFilter(table,filter)),compileAfter(table,query)),orderBy:query.sort.map(({column,direction})=>direction==="asc"?asc(columnOf(table,column)):desc(columnOf(table,column)))});import{AbstractRepository,getCurrentTenant as getCurrentTenant3,getTenancyConfig as getTenancyConfig3,getTenancyStrategy,Query}from"@elysiumjs/mnemosyne";import{eq as eq2}from"drizzle-orm";var DrizzleRepository=(model)=>{let table=()=>model.table;class R extends AbstractRepository(model,Database){async withConnection(operation){let name=this.constructor.connection;if(getTenancyConfig3().mode!=="rls")return operation(this.db);return(getTenancyStrategy()??new DrizzleRLSTenancy).withIsolation(Database.getBaseConnection(name),getCurrentTenant3()??"public",()=>operation(this.db))}async all(){return this.withConnection((db)=>db.select().from(table()))}async paginate(count,page=1){let offset=Math.max(page-1,0)*count;return this.withConnection(async(db)=>({page,total:await db.$count(table()),data:await db.select().from(table()).offset(offset).limit(count)}))}async find(id){let[row]=await this.withConnection((db)=>db.select().from(table()).where(eq2(table().id,id)));return row??null}async findBy(column,value){let[row]=await this.withConnection((db)=>db.select().from(table()).where(eq2(table()[column],value)));return row??null}async insert(data){let[row]=await this.withConnection((db)=>db.insert(table()).values(data).returning());return row}async update(id,data){let[row]=await this.withConnection((db)=>db.update(table()).set(data).where(eq2(table().id,id)).returning());return row??null}async updateAll(data){return this.withConnection((db)=>db.update(table()).set(data).returning())}async delete(id){let[row]=await this.withConnection((db)=>db.delete(table()).where(eq2(table().id,id)).returning());return row??null}async deleteAll(){return this.withConnection((db)=>db.delete(table()).returning())}async list(query){let{where,orderBy}=compileQuery(table(),query),rows=await this.withConnection((db)=>db.select().from(table()).where(where).orderBy(...orderBy).limit(query.limit+1));return Query.page(query,rows)}async count(query){let where=query?compileQuery(table(),{...query,after:void 0}).where:void 0;return this.withConnection((db)=>db.$count(table(),where))}async exists(id){return await this.withConnection((db)=>db.$count(table(),eq2(table().id,id)))>0}}return R};export{DEFAULT_CACHE_TTL,Database,DrizzleDatabaseCache,DrizzleModel,DrizzleModule,DrizzleRLSTenancy,DrizzleRepository,DrizzleSchemaTenancy,MissingDriverError,TenantMismatchError,compileAfter,compileFilter,compileQuery,createRLSPolicy,createSchemaFromDrizzle,drizzleAdapter,getSessionTenant,getTableBuilders,getTenantSchema,pgTable,registerTenantSchema,withRLS,wrapTenantSchema};
@@ -31,3 +31,15 @@ export declare const getMigrationName: (path: string) => string;
31
31
  * @param applied The applied migrations.
32
32
  */
33
33
  export declare const planMigrations: (files: string[], applied: AppliedMigration[]) => MigrationPlan;
34
+ /**
35
+ * Removes the `"public".` qualifier Drizzle Kit writes before identifiers, so that a migration
36
+ * runs in the schema selected by the `search_path` (the tenant schema).
37
+ *
38
+ * Only identifier qualifiers are changed: string literals, dollar-quoted bodies (functions, `DO`
39
+ * blocks), comments and other quoted identifiers are kept as is. Qualifiers inside a `DO` block
40
+ * are therefore kept: such blocks are not rewritten, since their contents are not parsed here.
41
+ * @author Axel Nana <axel.nana@workbud.com>
42
+ * @param sql The migration SQL.
43
+ * @returns The SQL without the public schema qualifiers.
44
+ */
45
+ export declare const stripPublicSchema: (sql: string) => string;
package/dist/model.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ModelAdapter } from '@elysiumjs/mnemosyne';
2
- import type { PgTableWithColumns, TableConfig } from 'drizzle-orm/pg-core';
2
+ import type { PgTable, PgTableWithColumns } from 'drizzle-orm/pg-core';
3
3
  /**
4
4
  * Drizzle-specific model adapter.
5
5
  *
@@ -22,7 +22,7 @@ export declare const drizzleAdapter: ModelAdapter<PgTableWithColumns<any>>;
22
22
  */
23
23
  export declare const createSchemaFromDrizzle: (table: PgTableWithColumns<any>, opts?: {
24
24
  mode?: 'create' | 'update' | 'select';
25
- }) => import("@sinclair/typebox").TObject<{}>;
25
+ }) => import("typebox").TObject<{}>;
26
26
  /**
27
27
  * Options for creating a DrizzleModel.
28
28
  */
@@ -41,7 +41,7 @@ export interface DrizzleModelOptions {
41
41
  * @param options Model configuration options.
42
42
  * @returns A model class with Drizzle-backed static metadata.
43
43
  */
44
- export declare const DrizzleModel: <TTable extends PgTableWithColumns<TableConfig>>(table: TTable, options?: DrizzleModelOptions) => {
44
+ export declare const DrizzleModel: <TTable extends PgTable>(table: TTable, options?: DrizzleModelOptions) => {
45
45
  new (): {};
46
46
  readonly $inferSelect: unknown;
47
47
  readonly $inferInsert: unknown;
@@ -49,13 +49,13 @@ export declare const DrizzleModel: <TTable extends PgTableWithColumns<TableConfi
49
49
  readonly table: any;
50
50
  readonly tableName: string;
51
51
  readonly columns: Record<string, import("drizzle-orm/pg-core").AnyPgColumnBuilder>;
52
- readonly insertSchema: import("@sinclair/typebox").TObject<{}>;
53
- readonly updateSchema: import("@sinclair/typebox").TObject<{}>;
54
- readonly selectSchema: import("@sinclair/typebox").TObject<{}>;
52
+ readonly insertSchema: import("typebox").TObject<{}>;
53
+ readonly updateSchema: import("typebox").TObject<{}>;
54
+ readonly selectSchema: import("typebox").TObject<{}>;
55
55
  readonly supportTenancy: boolean;
56
56
  } & {
57
- readonly $inferSelect: TTable["$inferSelect"];
58
- readonly $inferInsert: TTable["$inferInsert"];
59
- readonly $inferUpdate: Partial<TTable["$inferInsert"]>;
57
+ readonly $inferSelect: { [Key in keyof TTable["_"]["columns"] & string]: TTable["_"]["columns"][Key]["_"]["notNull"] extends true ? TTable["_"]["columns"][Key]["_"]["data"] : TTable["_"]["columns"][Key]["_"]["data"] | null; } extends infer T ? { [K in keyof T]: T[K]; } : never;
58
+ readonly $inferInsert: { [Key in keyof TTable["_"]["columns"] & string as import("drizzle-orm").RequiredKeyOnly<Key, TTable["_"]["columns"][Key]>]: TTable["_"]["columns"][Key]["_"]["notNull"] extends true ? TTable["_"]["columns"][Key]["_"]["data"] : TTable["_"]["columns"][Key]["_"]["data"] | null; } & { [Key in keyof TTable["_"]["columns"] & string as import("drizzle-orm").OptionalKeyOnly<Key, TTable["_"]["columns"][Key], false>]?: (TTable["_"]["columns"][Key]["_"]["notNull"] extends true ? TTable["_"]["columns"][Key]["_"]["data"] : TTable["_"]["columns"][Key]["_"]["data"] | null) | undefined; } extends infer T_1 ? { [K in keyof T_1]: T_1[K]; } : never;
59
+ readonly $inferUpdate: Partial<{ [Key in keyof TTable["_"]["columns"] & string as import("drizzle-orm").RequiredKeyOnly<Key, TTable["_"]["columns"][Key]>]: TTable["_"]["columns"][Key]["_"]["notNull"] extends true ? TTable["_"]["columns"][Key]["_"]["data"] : TTable["_"]["columns"][Key]["_"]["data"] | null; } & { [Key in keyof TTable["_"]["columns"] & string as import("drizzle-orm").OptionalKeyOnly<Key, TTable["_"]["columns"][Key], false>]?: (TTable["_"]["columns"][Key]["_"]["notNull"] extends true ? TTable["_"]["columns"][Key]["_"]["data"] : TTable["_"]["columns"][Key]["_"]["data"] | null) | undefined; } extends infer T_1 ? { [K in keyof T_1]: T_1[K]; } : never>;
60
60
  readonly table: TTable;
61
61
  };
@@ -0,0 +1,28 @@
1
+ import type { ParsedFilter, ParsedQuery } from '@elysiumjs/mnemosyne';
2
+ import type { SQL } from 'drizzle-orm';
3
+ import type { PgTableWithColumns } from 'drizzle-orm/pg-core';
4
+ /**
5
+ * Compiles one filter of a parsed query to a Drizzle condition.
6
+ * @author Axel Nana <axel.nana@workbud.com>
7
+ * @param table The table queried.
8
+ * @param filter The filter.
9
+ */
10
+ export declare const compileFilter: (table: PgTableWithColumns<any>, filter: ParsedFilter) => SQL;
11
+ /**
12
+ * Compiles the condition that keeps the rows after the cursor, in sort order: a keyset condition
13
+ * over every sort key, the row key last.
14
+ * @author Axel Nana <axel.nana@workbud.com>
15
+ * @param table The table queried.
16
+ * @param query The parsed query, with `after` set.
17
+ */
18
+ export declare const compileAfter: (table: PgTableWithColumns<any>, query: ParsedQuery) => SQL | undefined;
19
+ /**
20
+ * Compiles a parsed query: the `where` condition (filters and cursor) and the `order by`.
21
+ * @author Axel Nana <axel.nana@workbud.com>
22
+ * @param table The table queried.
23
+ * @param query The parsed query.
24
+ */
25
+ export declare const compileQuery: (table: PgTableWithColumns<any>, query: ParsedQuery) => {
26
+ where: SQL<unknown> | undefined;
27
+ orderBy: SQL<unknown>[];
28
+ };
@@ -1,19 +1,34 @@
1
- import type { IdType, ModelClass } from '@elysiumjs/mnemosyne';
2
- import type { PgTableWithColumns } from 'drizzle-orm/pg-core';
1
+ import type { CursorPage, IdType, ModelClass, ParsedQuery } from '@elysiumjs/mnemosyne';
2
+ import type { PgColumn, PgTable } from 'drizzle-orm/pg-core';
3
3
  import type { DrizzleConnection } from './database';
4
4
  /**
5
5
  * Drizzle-specific repository mixin with concrete CRUD operations.
6
6
  *
7
7
  * Extends the abstract repository with Drizzle query builder calls
8
- * for all standard CRUD methods.
8
+ * for all standard CRUD methods. The model table must have an `id` column, used as the
9
+ * primary key by `find`, `update`, `delete` and `exists`.
9
10
  *
10
11
  * @author Axel Nana <axel.nana@workbud.com>
11
12
  * @template TModel The model class type.
12
13
  * @param model The model class to create the repository for.
13
14
  * @returns A concrete repository class with CRUD methods.
14
15
  */
15
- export declare const DrizzleRepository: <TModel extends ModelClass<any, any, any, PgTableWithColumns<any>>>(model: TModel) => {
16
+ export declare const DrizzleRepository: <TModel extends ModelClass<any, any, any, PgTable & {
17
+ id: PgColumn;
18
+ }>>(model: TModel) => {
16
19
  new (): {
20
+ /**
21
+ * Runs a database operation on the repository connection.
22
+ *
23
+ * In RLS tenancy mode, an operation that runs outside a transaction is wrapped in one
24
+ * scoped to the current tenant (through the tenancy strategy's `withIsolation`), so RLS
25
+ * policies apply to it. Inside an open transaction, the operation joins it when the
26
+ * transaction is bound to the current tenant, and throws `TenantMismatchError` otherwise
27
+ * (see `DrizzleRLSTenancy.withIsolation`).
28
+ * @param operation The operation to run with the connection to use.
29
+ * @returns The return value of the operation.
30
+ */
31
+ withConnection<T>(operation: (db: DrizzleConnection) => Promise<T>): Promise<T>;
17
32
  all(): Promise<TModel["$inferSelect"][]>;
18
33
  paginate(count: number, page?: number): Promise<{
19
34
  page: number;
@@ -23,10 +38,23 @@ export declare const DrizzleRepository: <TModel extends ModelClass<any, any, any
23
38
  find(id: IdType): Promise<TModel["$inferSelect"] | null>;
24
39
  findBy<TKey extends keyof TModel["$inferSelect"]>(column: TKey, value: TModel["$inferSelect"][TKey]): Promise<TModel["$inferSelect"] | null>;
25
40
  insert(data: TModel["$inferInsert"]): Promise<TModel["$inferSelect"]>;
26
- update(id: IdType, data: TModel["$inferUpdate"]): Promise<TModel["$inferSelect"]>;
41
+ update(id: IdType, data: TModel["$inferUpdate"]): Promise<TModel["$inferSelect"] | null>;
27
42
  updateAll(data: TModel["$inferUpdate"]): Promise<TModel["$inferSelect"][]>;
28
- delete(id: IdType): Promise<TModel["$inferSelect"]>;
43
+ delete(id: IdType): Promise<TModel["$inferSelect"] | null>;
29
44
  deleteAll(): Promise<TModel["$inferSelect"][]>;
45
+ /**
46
+ * Lists a page of rows matching a parsed query (see `Query.parse`): its filters, its sort and
47
+ * cursor pagination. One row more than the limit is fetched to tell whether a next page
48
+ * exists.
49
+ * @param query The parsed query.
50
+ * @returns The rows of the page and the cursor of the next one.
51
+ */
52
+ list(query: ParsedQuery): Promise<CursorPage<TModel["$inferSelect"]>>;
53
+ /**
54
+ * Counts the rows matching the filters of a parsed query, ignoring its cursor.
55
+ * @param query The parsed query.
56
+ */
57
+ count(query?: ParsedQuery): Promise<number>;
30
58
  exists(id: IdType): Promise<boolean>;
31
59
  readonly db: DrizzleConnection;
32
60
  };
package/dist/tenancy.d.ts CHANGED
@@ -6,6 +6,7 @@ import type { DrizzleConnection } from './database';
6
6
  * @author Axel Nana <axel.nana@workbud.com>
7
7
  * @param tenant The tenant identifier.
8
8
  * @returns The tenant's PostgreSQL schema.
9
+ * @throws {Error} If the tenant identifier is malformed.
9
10
  */
10
11
  export declare const getTenantSchema: (tenant: string) => PgSchema;
11
12
  /**
@@ -13,6 +14,7 @@ export declare const getTenantSchema: (tenant: string) => PgSchema;
13
14
  * @author Axel Nana <axel.nana@workbud.com>
14
15
  * @param tenant The tenant identifier.
15
16
  * @returns The newly registered (or existing) `PgSchema`.
17
+ * @throws {Error} If the tenant identifier is malformed.
16
18
  */
17
19
  export declare const registerTenantSchema: (tenant: string) => PgSchema;
18
20
  /**
@@ -26,26 +28,27 @@ export declare const registerTenantSchema: (tenant: string) => PgSchema;
26
28
  * @param tableName The table name.
27
29
  * @param columns The column definitions.
28
30
  * @returns A tenant-scoped `PgTable`.
31
+ * @throws {Error} If the tenant identifier is malformed.
29
32
  */
30
33
  export declare const wrapTenantSchema: <TTableName extends string, TColumnsMap extends Record<string, AnyPgColumnBuilder>>(tenant: string, tableName: TTableName, columns: TColumnsMap) => PgTable;
31
34
  /**
32
35
  * Executes a callback within an RLS-scoped transaction for the given tenant.
36
+ *
37
+ * The transaction is registered in the async context, so repositories and
38
+ * `Database.getConnection()` use it inside the callback. When a transaction is already open on the
39
+ * connection, the callback joins it if it is bound to the same tenant. Work for several tenants
40
+ * (such as an admin fan-out) must use one `withRLS()` per tenant, outside any transaction.
33
41
  * @author Axel Nana <axel.nana@workbud.com>
34
42
  * @param tenant The tenant identifier (or `null` for 'public').
35
43
  * @param callback The function to execute within the transaction.
36
44
  * @param connectionName The database connection name.
37
45
  * @returns The return value of the callback.
46
+ * @throws {TenantMismatchError} If the open transaction is bound to another tenant.
38
47
  */
39
48
  export declare const withRLS: <T>(tenant: string | null, callback: (tx: PgAsyncTransaction<any, any>) => Promise<T>, connectionName?: string) => Promise<T>;
40
49
  /**
41
- * Sets the current tenant on a persistent database connection (session-level).
42
- * @author Axel Nana <axel.nana@workbud.com>
43
- * @param tenant The tenant identifier (or `null` for 'public').
44
- * @param connectionName The database connection name.
45
- */
46
- export declare const setConnectionTenant: (tenant: string | null, connectionName?: string) => Promise<void>;
47
- /**
48
- * Reads the current tenant from the PostgreSQL session variable.
50
+ * Reads the current tenant from the PostgreSQL tenant variable, on the open transaction when
51
+ * there is one.
49
52
  * @author Axel Nana <axel.nana@workbud.com>
50
53
  * @param connectionName The database connection name.
51
54
  * @returns The current tenant identifier, or `null`.
@@ -84,5 +87,19 @@ export declare class DrizzleSchemaTenancy implements TenancyStrategy<PgTable, Dr
84
87
  export declare class DrizzleRLSTenancy implements TenancyStrategy<PgTable, DrizzleConnection> {
85
88
  readonly mode = "rls";
86
89
  resolveTable(table: PgTable, _tenant: string): PgTable;
90
+ /**
91
+ * Runs a callback in a transaction scoped to the given tenant.
92
+ *
93
+ * The transaction is bound to the tenant for its whole life, registered in the async context
94
+ * for the connection, and the tenant set as the current tenant, for the duration of the
95
+ * callback. When a transaction is already open on the connection, the callback joins it, which
96
+ * is only allowed when that transaction is bound to the same tenant.
97
+ * @author Axel Nana <axel.nana@workbud.com>
98
+ * @param connection The base connection (see `Database.getBaseConnection`).
99
+ * @param tenant The tenant identifier.
100
+ * @param callback The function to execute within the isolated context.
101
+ * @returns The return value of the callback.
102
+ * @throws {TenantMismatchError} If the open transaction is bound to another tenant.
103
+ */
87
104
  withIsolation<T>(connection: DrizzleConnection, tenant: string, callback: () => Promise<T>): Promise<T>;
88
105
  }
package/package.json CHANGED
@@ -1,22 +1,29 @@
1
1
  {
2
2
  "name": "@elysiumjs/mnemosyne-drizzle",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Drizzle ORM driver for @elysiumjs/mnemosyne",
6
6
  "private": false,
7
7
  "author": "Workbud Technologies Inc.",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/workbud/elysium"
10
+ "url": "git+https://github.com/na2axl/elysiumjs.git",
11
+ "directory": "@elysiumjs/mnemosyne-drizzle"
12
+ },
13
+ "homepage": "https://github.com/na2axl/elysiumjs/tree/main/@elysiumjs/mnemosyne-drizzle#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/na2axl/elysiumjs/issues"
11
16
  },
12
- "homepage": "https://github.com/workbud/elysium",
13
17
  "scripts": {
14
18
  "format": "prettier --write \"./src/**/*.ts\"",
15
19
  "clean": "rimraf dist",
16
- "build": "bun run clean && tsc && bun run build:minify",
17
- "build:minify": "bun scripts/minify.ts",
18
- "typecheck": "tsc --noEmit",
19
- "prepublishOnly": "bun run build"
20
+ "build": "bun run clean && bun run build:js && bun run build:ts",
21
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.types.json",
22
+ "prepublishOnly": "bun run build",
23
+ "test": "bun test",
24
+ "test:coverage": "bun test --coverage",
25
+ "build:js": "bun build ./src/index.ts ./src/drivers/bun-sql.ts ./src/drivers/postgres-js.ts ./src/drivers/node-postgres.ts ./src/drivers/hyperdrive.ts --outdir ./dist --target node --packages external --root ./src --minify-syntax --minify-whitespace",
26
+ "build:ts": "tsc --emitDeclarationOnly"
20
27
  },
21
28
  "keywords": [
22
29
  "elysium",
@@ -28,10 +35,39 @@
28
35
  "module": "dist/index.js",
29
36
  "type": "module",
30
37
  "types": "dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js"
42
+ },
43
+ "./bun-sql": {
44
+ "types": "./dist/drivers/bun-sql.d.ts",
45
+ "import": "./dist/drivers/bun-sql.js"
46
+ },
47
+ "./postgres-js": {
48
+ "types": "./dist/drivers/postgres-js.d.ts",
49
+ "import": "./dist/drivers/postgres-js.js"
50
+ },
51
+ "./node-postgres": {
52
+ "types": "./dist/drivers/node-postgres.d.ts",
53
+ "import": "./dist/drivers/node-postgres.js"
54
+ },
55
+ "./hyperdrive": {
56
+ "types": "./dist/drivers/hyperdrive.d.ts",
57
+ "import": "./dist/drivers/hyperdrive.js"
58
+ }
59
+ },
60
+ "files": [
61
+ "dist",
62
+ "CHANGELOG.md"
63
+ ],
31
64
  "sideEffects": false,
32
65
  "peerDependencies": {
33
- "@elysiumjs/core": "^0.8.0",
34
- "@elysiumjs/mnemosyne": "^0.2.0"
66
+ "@elysiumjs/core": "^0.11.0",
67
+ "@elysiumjs/mnemosyne": "^0.4.0",
68
+ "@elysiumjs/runtime-workers": "^0.1.0",
69
+ "pg": "^8.0.0",
70
+ "postgres": "^3.4.0"
35
71
  },
36
72
  "dependencies": {
37
73
  "drizzle-orm": "^1.0.0-rc.5",
@@ -39,8 +75,24 @@
39
75
  "radash": "^12.1.1"
40
76
  },
41
77
  "devDependencies": {
78
+ "@elysiumjs/core": "0.11.0",
79
+ "@elysiumjs/mnemosyne": "0.4.0",
80
+ "@elysiumjs/runtime-bun": "0.1.0",
81
+ "@types/pg": "8.15.5",
42
82
  "@types/prompts": "^2.4.9",
43
- "@elysiumjs/core": "0.8.0",
44
- "@elysiumjs/mnemosyne": "0.2.0"
83
+ "pg": "8.16.3",
84
+ "postgres": "3.4.7",
85
+ "@elysiumjs/runtime-workers": "0.1.0"
86
+ },
87
+ "peerDependenciesMeta": {
88
+ "@elysiumjs/runtime-workers": {
89
+ "optional": true
90
+ },
91
+ "pg": {
92
+ "optional": true
93
+ },
94
+ "postgres": {
95
+ "optional": true
96
+ }
45
97
  }
46
98
  }
@@ -1,7 +0,0 @@
1
- {"t":0,"agent":"aac206b","agent_type":"executor","event":"agent_start","parent_mode":"none"}
2
- {"t":0,"agent":"a914068","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":31478}
3
- {"t":0,"agent":"aa0cc16","agent_type":"untracked-native-fork","event":"agent_stop","success":true,"synthetic":true,"telemetry_status":"unmatched_stop","reason":"SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."}
4
- {"t":0,"agent":"a55f688","agent_type":"untracked-native-fork","event":"agent_stop","success":true,"synthetic":true,"telemetry_status":"unmatched_stop","reason":"SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."}
5
- {"t":0,"agent":"a0ba584","agent_type":"untracked-native-fork","event":"agent_stop","success":true,"synthetic":true,"telemetry_status":"unmatched_stop","reason":"SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."}
6
- {"t":0,"agent":"a6e8774","agent_type":"untracked-native-fork","event":"agent_stop","success":true,"synthetic":true,"telemetry_status":"unmatched_stop","reason":"SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."}
7
- {"t":0,"agent":"aac206b","agent_type":"executor","event":"agent_stop","success":true,"duration_ms":168112}
@@ -1,7 +0,0 @@
1
- {
2
- "tool_name": "Bash",
3
- "tool_input_preview": "{\"command\":\"perl -0pi -e 's/\\\\t\\\\t\\\\tconst migrations: \\\\{ name: string \\\\}\\\\[\\\\] = await sql`SELECT name FROM elysium_migrations;`;\\\\n\\\\t\\\\t\\\\tconst completedMigrations = migrations\\\\.map\\\\(\\\\(row\\\\)...",
4
- "error": "Exit code 1\n\t\t\t}\n\n\t\t\tconst rows: { name: string; applied_at: Date }[] =\n\t\t\t\tawait sql`SELECT name, applied_at FROM elysium_migrations;`;\n\t\t\tconst completedMigrations = rows.map((row) => ({ name: row.name, appliedAt: row.applied_at }));\n\n\t\t\tconst filesIt = new Bun.Glob(`./src/database/migrations/**/*.sql`).scan({\n\t\t\t\tabsolute: true,\n\t\t\t\tonlyFiles: true\n\t\t\t});\n\n\t\t\tconst files = await Array.fromAsync(filesIt);\n\t\t\tconst { pending: sqlFiles, renames } = planMigrations(files, completedMigrations);\nChe...",
5
- "timestamp": "2026-10-26T02:29:07.154Z",
6
- "retry_count": 1
7
- }
@@ -1,93 +0,0 @@
1
- {
2
- "updatedAt": "2026-10-24T23:32:53.900Z",
3
- "missions": [
4
- {
5
- "id": "session:3920b579-6d8e-4f5f-924a-319202936685:none",
6
- "source": "session",
7
- "name": "none",
8
- "objective": "Session mission",
9
- "createdAt": "2026-10-24T23:30:05.788Z",
10
- "updatedAt": "2026-10-24T23:32:53.900Z",
11
- "status": "done",
12
- "workerCount": 1,
13
- "taskCounts": {
14
- "total": 1,
15
- "pending": 0,
16
- "blocked": 0,
17
- "inProgress": 0,
18
- "completed": 1,
19
- "failed": 0
20
- },
21
- "agents": [
22
- {
23
- "name": "executor:aac206b",
24
- "role": "executor",
25
- "ownership": "aac206b9ab7956eb4",
26
- "status": "done",
27
- "currentStep": null,
28
- "latestUpdate": "completed",
29
- "completedSummary": null,
30
- "updatedAt": "2026-10-24T23:32:53.900Z"
31
- }
32
- ],
33
- "timeline": [
34
- {
35
- "id": "session-start:aac206b9ab7956eb4:2026-10-24T23:30:05.788Z",
36
- "at": "2026-10-24T23:30:05.788Z",
37
- "kind": "update",
38
- "agent": "executor:aac206b",
39
- "detail": "started executor:aac206b",
40
- "sourceKey": "session-start:aac206b9ab7956eb4"
41
- },
42
- {
43
- "id": "session-stop:a914068b2d3b26e56:2026-10-24T23:30:37.266Z",
44
- "at": "2026-10-24T23:30:37.266Z",
45
- "kind": "completion",
46
- "agent": "executor:aac206b",
47
- "detail": "completed",
48
- "sourceKey": "session-stop:a914068b2d3b26e56"
49
- },
50
- {
51
- "id": "session-stop:aa0cc16535a1e4c83:2026-10-24T23:31:08.989Z",
52
- "at": "2026-10-24T23:31:08.989Z",
53
- "kind": "completion",
54
- "agent": "executor:aac206b",
55
- "detail": "completed",
56
- "sourceKey": "session-stop:aa0cc16535a1e4c83"
57
- },
58
- {
59
- "id": "session-stop:a55f6887513d75db7:2026-10-24T23:31:40.515Z",
60
- "at": "2026-10-24T23:31:40.515Z",
61
- "kind": "completion",
62
- "agent": "executor:aac206b",
63
- "detail": "completed",
64
- "sourceKey": "session-stop:a55f6887513d75db7"
65
- },
66
- {
67
- "id": "session-stop:a0ba584b3d851e834:2026-10-24T23:32:12.704Z",
68
- "at": "2026-10-24T23:32:12.704Z",
69
- "kind": "completion",
70
- "agent": "executor:aac206b",
71
- "detail": "completed",
72
- "sourceKey": "session-stop:a0ba584b3d851e834"
73
- },
74
- {
75
- "id": "session-stop:a6e8774450f70f52b:2026-10-24T23:32:44.262Z",
76
- "at": "2026-10-24T23:32:44.262Z",
77
- "kind": "completion",
78
- "agent": "executor:aac206b",
79
- "detail": "completed",
80
- "sourceKey": "session-stop:a6e8774450f70f52b"
81
- },
82
- {
83
- "id": "session-stop:aac206b9ab7956eb4:2026-10-24T23:32:53.900Z",
84
- "at": "2026-10-24T23:32:53.900Z",
85
- "kind": "completion",
86
- "agent": "executor:aac206b",
87
- "detail": "completed",
88
- "sourceKey": "session-stop:aac206b9ab7956eb4"
89
- }
90
- ]
91
- }
92
- ]
93
- }
@@ -1,18 +0,0 @@
1
- {
2
- "version": 1,
3
- "entries": {
4
- "445ed27a3872b681d98190bae61ccb84954e1bc4e140df5370be958dee776b3a": {
5
- "last_emitted_at_ms": 1792981759903,
6
- "message": "Verify changes work after editing. Test functionality before marking complete."
7
- },
8
- "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
9
- "last_emitted_at_ms": 1792981745167,
10
- "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
11
- },
12
- "466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
13
- "last_emitted_at_ms": 1792981448165,
14
- "message": "Read multiple files in parallel when possible for faster analysis."
15
- }
16
- },
17
- "updated_at": "2026-10-26T02:29:19.903Z"
18
- }
@@ -1,61 +0,0 @@
1
- {
2
- "agents": [
3
- {
4
- "agent_id": "aac206b9ab7956eb4",
5
- "agent_type": "oh-my-claudecode:executor",
6
- "started_at": "2026-10-24T23:30:05.788Z",
7
- "parent_mode": "none",
8
- "status": "completed",
9
- "completed_at": "2026-10-24T23:32:53.900Z",
10
- "duration_ms": 168112
11
- },
12
- {
13
- "agent_id": "aa0cc16535a1e4c83",
14
- "agent_type": "untracked-native-fork",
15
- "started_at": "2026-10-24T23:31:08.989Z",
16
- "parent_mode": "none",
17
- "status": "completed",
18
- "completed_at": "2026-10-24T23:31:08.989Z",
19
- "synthetic": true,
20
- "telemetry_status": "unmatched_stop",
21
- "telemetry_note": "SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."
22
- },
23
- {
24
- "agent_id": "a55f6887513d75db7",
25
- "agent_type": "untracked-native-fork",
26
- "started_at": "2026-10-24T23:31:40.515Z",
27
- "parent_mode": "none",
28
- "status": "completed",
29
- "completed_at": "2026-10-24T23:31:40.515Z",
30
- "synthetic": true,
31
- "telemetry_status": "unmatched_stop",
32
- "telemetry_note": "SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."
33
- },
34
- {
35
- "agent_id": "a0ba584b3d851e834",
36
- "agent_type": "untracked-native-fork",
37
- "started_at": "2026-10-24T23:32:12.704Z",
38
- "parent_mode": "none",
39
- "status": "completed",
40
- "completed_at": "2026-10-24T23:32:12.704Z",
41
- "synthetic": true,
42
- "telemetry_status": "unmatched_stop",
43
- "telemetry_note": "SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."
44
- },
45
- {
46
- "agent_id": "a6e8774450f70f52b",
47
- "agent_type": "untracked-native-fork",
48
- "started_at": "2026-10-24T23:32:44.262Z",
49
- "parent_mode": "none",
50
- "status": "completed",
51
- "completed_at": "2026-10-24T23:32:44.262Z",
52
- "synthetic": true,
53
- "telemetry_status": "unmatched_stop",
54
- "telemetry_note": "SubagentStop arrived without a matching SubagentStart; native Agent/Task start telemetry was not observed."
55
- }
56
- ],
57
- "total_spawned": 1,
58
- "total_completed": 6,
59
- "total_failed": 0,
60
- "last_updated": "2026-10-24T23:32:53.907Z"
61
- }
@@ -1,3 +0,0 @@
1
- $ bun run clean && tsc && bun run build:minify
2
- $ rimraf dist
3
- $ bun scripts/minify.ts
@@ -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};var __metadata=function(k,v){if(typeof Reflect==="object"&&typeof Reflect.metadata==="function")return Reflect.metadata(k,v)};import{existsSync}from"node:fs";import{exists}from"node:fs/promises";import{dirname,join}from"node:path";import{Command,CommandArgumentType}from"@elysiumjs/core";import prompts from"prompts";const getProjectPath=()=>{const startPath=process.cwd();let dir=startPath;while(true){if(existsSync(join(dir,".elysiumrc")))return dir;const parentDir=dirname(dir);if(dir===parentDir)break;dir=parentDir}return startPath};let MigrationGenerateCommand=class MigrationGenerateCommand extends Command{static command="migration:generate";static description="Generates migration files.";name;async run(){if(!this.name){const answers=await prompts([{type:"text",name:"name",message:"Migration Name:",initial:"create_users_table",validate(value){if(value.length<1){return"Migration name cannot be empty"}return true}}],{onCancel:()=>{this.error("Operation cancelled.");process.exit(0)}});this.name=answers.name}const projectRoot=getProjectPath();let schemaPath=`${projectRoot}/src/database/schemas`;if(await exists(`${projectRoot}/src/database/schema.ts`)){schemaPath=`${projectRoot}/src/database/schema.ts`}await Bun.$`bunx --bun drizzle-kit generate --dialect postgresql --schema ${schemaPath} --out ${projectRoot}/src/database/migrations --name ${this.name}`;const sqlFiles=new Bun.Glob(`${projectRoot}/src/database/migrations/**/*.sql`).scan();for await(const path of sqlFiles){const file=Bun.file(path);const content=await file.text();await file.write(content.replace(/"public"\./gm,""))}}};__decorate([Command.arg({description:"The name of the migration to create",type:CommandArgumentType.STRING}),__metadata("design:type",String)],MigrationGenerateCommand.prototype,"name",undefined);MigrationGenerateCommand=__decorate([Command.register()],MigrationGenerateCommand);export{MigrationGenerateCommand};