@elysiumjs/mnemosyne-drizzle 0.1.4 → 0.2.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/.omc/state/agent-replay-3920b579-6d8e-4f5f-924a-319202936685.jsonl +7 -0
  2. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/last-tool-error-state.json +7 -0
  3. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/mission-state.json +93 -0
  4. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/pre-tool-advisory-throttle.json +18 -0
  5. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/subagent-tracking-state.json +61 -0
  6. package/.turbo/turbo-build.log +3 -9
  7. package/CHANGELOG.md +40 -0
  8. package/README.md +51 -0
  9. package/dist/commands/migration_generate.command.d.ts +11 -0
  10. package/dist/commands/migration_generate.command.js +1 -0
  11. package/dist/commands/migration_run.command.d.ts +13 -0
  12. package/dist/commands/migration_run.command.js +15 -0
  13. package/dist/database.d.ts +9 -1
  14. package/dist/database.js +1 -0
  15. package/dist/drizzle.module.d.ts +40 -0
  16. package/dist/drizzle.module.js +1 -0
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.js +1 -2
  19. package/dist/migrations.d.ts +33 -0
  20. package/dist/migrations.js +1 -0
  21. package/dist/model.d.ts +5 -5
  22. package/dist/model.js +1 -0
  23. package/dist/repository.js +1 -0
  24. package/dist/table.d.ts +47 -123
  25. package/dist/table.js +1 -0
  26. package/dist/tenancy.d.ts +3 -3
  27. package/dist/tenancy.js +1 -0
  28. package/package.json +11 -9
  29. package/scripts/minify.ts +10 -0
  30. package/src/commands/migration_generate.command.ts +101 -0
  31. package/src/commands/migration_run.command.ts +160 -0
  32. package/src/database.ts +37 -7
  33. package/src/drizzle.module.ts +97 -0
  34. package/src/index.ts +1 -31
  35. package/src/migrations.ts +100 -0
  36. package/src/model.ts +18 -11
  37. package/src/table.ts +4 -4
  38. package/src/tenancy.ts +5 -5
  39. package/tests/drizzle.module.test.ts +201 -0
  40. package/tests/migrations.test.ts +123 -0
  41. package/tests/setup.ts +0 -10
  42. package/tsconfig.json +2 -2
  43. package/.turbo/turbo-typecheck.log +0 -1
  44. package/coverage/.lcov.info.01ffde0d9f956dce.tmp +0 -14
  45. package/coverage/.lcov.info.2f80a4def3a82099.tmp +0 -343
  46. package/coverage/.lcov.info.560a2daaba0934cd.tmp +0 -366
  47. package/coverage/.lcov.info.7162c9b788dc0bc6.tmp +0 -366
  48. package/coverage/.lcov.info.b57c0b452c3901e5.tmp +0 -17
  49. package/coverage/.lcov.info.c47de8626f331696.tmp +0 -17
  50. package/coverage/.lcov.info.c710a42b25b08349.tmp +0 -343
  51. package/coverage/.lcov.info.dbc8f065467fc8a2.tmp +0 -287
  52. package/coverage/.lcov.info.de180a43301f82c0.tmp +0 -35
  53. package/coverage/.lcov.info.df18163ab929ab1b.tmp +0 -357
  54. package/coverage/.lcov.info.febf711c192f5c0c.tmp +0 -17
  55. package/coverage/lcov.info +0 -357
  56. package/tests/database.test.ts +0 -198
package/dist/table.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { PgColumnBuilderBase, PgTableWithColumns } from 'drizzle-orm/pg-core';
1
+ import type { AnyPgColumnBuilder, PgTableWithColumns } from 'drizzle-orm/pg-core';
2
2
  /**
3
3
  * Retrieves the original column builders for a table.
4
4
  * @param table The table created with enhanced pgTable.
5
5
  * @returns The original column builder definitions, or `undefined`.
6
6
  */
7
- export declare const getTableBuilders: (table: PgTableWithColumns<any>) => Record<string, PgColumnBuilderBase> | undefined;
7
+ export declare const getTableBuilders: (table: PgTableWithColumns<any>) => Record<string, AnyPgColumnBuilder> | undefined;
8
8
  /**
9
9
  * Creates a PostgreSQL table with automatic column builder registration.
10
10
  *
@@ -17,129 +17,53 @@ export declare const getTableBuilders: (table: PgTableWithColumns<any>) => Recor
17
17
  * @param extraConfig Optional extra Drizzle table configuration.
18
18
  * @returns A Drizzle PostgreSQL table with registered column builders.
19
19
  */
20
- export declare const pgTable: <TTableName extends string, TColumns extends Record<string, PgColumnBuilderBase>>(tableName: TTableName, columns: TColumns, extraConfig?: (table: any) => any) => PgTableWithColumns<{
20
+ export declare const pgTable: <TTableName extends string, TColumns extends Record<string, AnyPgColumnBuilder>>(tableName: TTableName, columns: TColumns, extraConfig?: (table: any) => any) => PgTableWithColumns<{
21
21
  name: TTableName;
22
22
  schema: undefined;
23
- columns: { [Key in keyof TColumns]: import("drizzle-orm/pg-core").PgColumn<{
24
- name: (Omit<TColumns[Key]["_"], "name"> & {
25
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
26
- })["name"];
23
+ columns: { [Key in keyof TColumns]: import("drizzle-orm/pg-core").PgBuildColumn<TTableName, TColumns[Key], {
24
+ name: string;
27
25
  tableName: TTableName;
28
- dataType: (Omit<TColumns[Key]["_"], "name"> & {
29
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
30
- })["dataType"];
31
- columnType: (Omit<TColumns[Key]["_"], "name"> & {
32
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
33
- })["columnType"];
34
- data: Omit<TColumns[Key]["_"], "name"> & {
35
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
36
- } extends infer T ? T extends Omit<TColumns[Key]["_"], "name"> & {
37
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
38
- } ? T extends {
26
+ dataType: TColumns[Key]["_"]["dataType"];
27
+ data: TColumns[Key]["_"]["dimensions"] extends 1 | 2 | 3 | 4 | 5 ? TColumns[Key]["_"]["dimensions"] extends infer T ? T extends TColumns[Key]["_"]["dimensions"] ? T extends 1 ? (TColumns[Key]["_"] extends infer T_1 ? T_1 extends TColumns[Key]["_"] ? T_1 extends {
39
28
  $type: infer U;
40
- } ? U : T["data"] : never : never;
41
- driverParam: (Omit<TColumns[Key]["_"], "name"> & {
42
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
43
- })["driverParam"];
44
- notNull: Omit<TColumns[Key]["_"], "name"> & {
45
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
46
- } extends infer T_1 ? T_1 extends Omit<TColumns[Key]["_"], "name"> & {
47
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
48
- } ? T_1 extends {
49
- notNull: true;
50
- } ? true : false : never : never;
51
- hasDefault: Omit<TColumns[Key]["_"], "name"> & {
52
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
53
- } extends infer T_2 ? T_2 extends Omit<TColumns[Key]["_"], "name"> & {
54
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
55
- } ? T_2 extends {
56
- hasDefault: true;
57
- } ? true : false : never : never;
58
- isPrimaryKey: Omit<TColumns[Key]["_"], "name"> & {
59
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
60
- } extends infer T_3 ? T_3 extends Omit<TColumns[Key]["_"], "name"> & {
61
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
62
- } ? T_3 extends {
63
- isPrimaryKey: true;
64
- } ? true : false : never : never;
65
- isAutoincrement: Omit<TColumns[Key]["_"], "name"> & {
66
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
67
- } extends infer T_4 ? T_4 extends Omit<TColumns[Key]["_"], "name"> & {
68
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
69
- } ? T_4 extends {
70
- isAutoincrement: true;
71
- } ? true : false : never : never;
72
- hasRuntimeDefault: Omit<TColumns[Key]["_"], "name"> & {
73
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
74
- } extends infer T_5 ? T_5 extends Omit<TColumns[Key]["_"], "name"> & {
75
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
76
- } ? T_5 extends {
77
- hasRuntimeDefault: true;
78
- } ? true : false : never : never;
79
- enumValues: (Omit<TColumns[Key]["_"], "name"> & {
80
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
81
- })["enumValues"];
82
- baseColumn: Omit<TColumns[Key]["_"], "name"> & {
83
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
84
- } extends infer T_6 ? T_6 extends Omit<TColumns[Key]["_"], "name"> & {
85
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
86
- } ? T_6 extends {
87
- baseBuilder: infer U_1 extends import("drizzle-orm").ColumnBuilderBase;
88
- } ? import("drizzle-orm").Column<{
89
- name: U_1["_"]["name"];
90
- tableName: TTableName;
91
- dataType: U_1["_"]["dataType"];
92
- columnType: U_1["_"]["columnType"];
93
- data: U_1["_"] extends infer T_7 ? T_7 extends U_1["_"] ? T_7 extends {
94
- $type: infer U;
95
- } ? U : T_7["data"] : never : never;
96
- driverParam: U_1["_"]["driverParam"];
97
- notNull: U_1["_"] extends infer T_8 ? T_8 extends U_1["_"] ? T_8 extends {
98
- notNull: true;
99
- } ? true : false : never : never;
100
- hasDefault: U_1["_"] extends infer T_9 ? T_9 extends U_1["_"] ? T_9 extends {
101
- hasDefault: true;
102
- } ? true : false : never : never;
103
- isPrimaryKey: U_1["_"] extends infer T_10 ? T_10 extends U_1["_"] ? T_10 extends {
104
- isPrimaryKey: true;
105
- } ? true : false : never : never;
106
- isAutoincrement: U_1["_"] extends infer T_11 ? T_11 extends U_1["_"] ? T_11 extends {
107
- isAutoincrement: true;
108
- } ? true : false : never : never;
109
- hasRuntimeDefault: U_1["_"] extends infer T_12 ? T_12 extends U_1["_"] ? T_12 extends {
110
- hasRuntimeDefault: true;
111
- } ? true : false : never : never;
112
- enumValues: U_1["_"]["enumValues"];
113
- baseColumn: U_1["_"] extends infer T_13 ? T_13 extends U_1["_"] ? T_13 extends {
114
- baseBuilder: infer U_1 extends import("drizzle-orm").ColumnBuilderBase;
115
- } ? import("drizzle-orm").Column</*elided*/ any, {}, Omit<U_1["_"], "name" | "data" | "driverParam" | "tableName" | "notNull" | "hasDefault" | "isPrimaryKey" | "isAutoincrement" | "hasRuntimeDefault" | "dataType" | "columnType" | "enumValues" | "identity" | "dialect" | "baseColumn" | "generated" | "brand"> extends infer T_14 ? { [K in keyof T_14]: T_14[K]; } : never> : never : never : never;
116
- identity: U_1["_"] extends infer T_15 ? T_15 extends U_1["_"] ? T_15 extends {
117
- identity: "always";
118
- } ? "always" : T_15 extends {
119
- identity: "byDefault";
120
- } ? "byDefault" : undefined : never : never;
121
- generated: U_1["_"] extends infer T_16 ? T_16 extends U_1["_"] ? T_16 extends {
122
- generated: infer G;
123
- } ? unknown extends G ? undefined : G extends undefined ? undefined : G : undefined : never : never;
124
- }, {}, Omit<U_1["_"], "name" | "data" | "driverParam" | "tableName" | "notNull" | "hasDefault" | "isPrimaryKey" | "isAutoincrement" | "hasRuntimeDefault" | "dataType" | "columnType" | "enumValues" | "identity" | "dialect" | "baseColumn" | "generated" | "brand"> extends infer T_17 ? { [K in keyof T_17]: T_17[K]; } : never> : never : never : never;
125
- identity: Omit<TColumns[Key]["_"], "name"> & {
126
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
127
- } extends infer T_18 ? T_18 extends Omit<TColumns[Key]["_"], "name"> & {
128
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
129
- } ? T_18 extends {
130
- identity: "always";
131
- } ? "always" : T_18 extends {
132
- identity: "byDefault";
133
- } ? "byDefault" : undefined : never : never;
134
- generated: Omit<TColumns[Key]["_"], "name"> & {
135
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
136
- } extends infer T_19 ? T_19 extends Omit<TColumns[Key]["_"], "name"> & {
137
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
138
- } ? T_19 extends {
139
- generated: infer G;
140
- } ? unknown extends G ? undefined : G extends undefined ? undefined : G : undefined : never : never;
141
- }, {}, Omit<Omit<TColumns[Key]["_"], "name"> & {
142
- name: TColumns[Key]["_"]["name"] extends "" ? import("drizzle-orm").Assume<Key, string> : TColumns[Key]["_"]["name"];
143
- }, "name" | "data" | "driverParam" | "tableName" | "notNull" | "hasDefault" | "isPrimaryKey" | "isAutoincrement" | "hasRuntimeDefault" | "dataType" | "columnType" | "enumValues" | "identity" | "dialect" | "baseColumn" | "generated" | "brand"> extends infer T_20 ? { [K_1 in keyof T_20]: T_20[K_1]; } : never>; };
144
- dialect: "pg";
29
+ } ? U : T_1 extends {
30
+ data: infer D;
31
+ } ? D : unknown : never : never)[] : T extends 2 ? (TColumns[Key]["_"] extends infer T_1 ? T_1 extends TColumns[Key]["_"] ? T_1 extends {
32
+ $type: infer U;
33
+ } ? U : T_1 extends {
34
+ data: infer D;
35
+ } ? D : unknown : never : never)[][] : T extends 3 ? (TColumns[Key]["_"] extends infer T_1 ? T_1 extends TColumns[Key]["_"] ? T_1 extends {
36
+ $type: infer U;
37
+ } ? U : T_1 extends {
38
+ data: infer D;
39
+ } ? D : unknown : never : never)[][][] : T extends 4 ? (TColumns[Key]["_"] extends infer T_1 ? T_1 extends TColumns[Key]["_"] ? T_1 extends {
40
+ $type: infer U;
41
+ } ? U : T_1 extends {
42
+ data: infer D;
43
+ } ? D : unknown : never : never)[][][][] : T extends 5 ? (TColumns[Key]["_"] extends infer T_1 ? T_1 extends TColumns[Key]["_"] ? T_1 extends {
44
+ $type: infer U;
45
+ } ? U : T_1 extends {
46
+ data: infer D;
47
+ } ? D : unknown : never : never)[][][][][] : TColumns[Key]["_"] extends infer T_1 ? T_1 extends TColumns[Key]["_"] ? T_1 extends {
48
+ $type: infer U;
49
+ } ? U : T_1 extends {
50
+ data: infer D;
51
+ } ? D : unknown : never : never : never : never : TColumns[Key]["_"] extends infer T_1 ? T_1 extends TColumns[Key]["_"] ? T_1 extends {
52
+ $type: infer U;
53
+ } ? U : T_1 extends {
54
+ data: infer D;
55
+ } ? D : unknown : never : never;
56
+ driverParam: TColumns[Key]["_"]["dimensions"] extends 1 | 2 | 3 | 4 | 5 ? string | (TColumns[Key]["_"]["dimensions"] extends infer T_2 ? T_2 extends TColumns[Key]["_"]["dimensions"] ? T_2 extends 1 ? TColumns[Key]["_"]["driverParam"][] : T_2 extends 2 ? TColumns[Key]["_"]["driverParam"][][] : T_2 extends 3 ? TColumns[Key]["_"]["driverParam"][][][] : T_2 extends 4 ? TColumns[Key]["_"]["driverParam"][][][][] : T_2 extends 5 ? TColumns[Key]["_"]["driverParam"][][][][][] : TColumns[Key]["_"]["driverParam"] : never : never) : TColumns[Key]["_"]["driverParam"];
57
+ notNull: TColumns[Key]["_"]["notNull"] extends true ? true : false;
58
+ hasDefault: TColumns[Key]["_"]["hasDefault"] extends true ? true : false;
59
+ isPrimaryKey: false;
60
+ isAutoincrement: false;
61
+ hasRuntimeDefault: false;
62
+ enumValues: TColumns[Key]["_"] extends infer T_3 ? T_3 extends TColumns[Key]["_"] ? T_3 extends {
63
+ enumValues: infer E extends string[];
64
+ } ? E : undefined : never : never;
65
+ identity: TColumns[Key]["_"]["identity"] extends "always" | "byDefault" ? TColumns[Key]["_"]["identity"] : undefined;
66
+ generated: TColumns[Key]["_"]["generated"] extends true ? true : undefined;
67
+ }>; };
68
+ dialect: 'pg';
145
69
  }>;
package/dist/table.js ADDED
@@ -0,0 +1 @@
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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { TenancyStrategy } from '@elysiumjs/mnemosyne';
2
- import type { PgColumnBuilderBase, PgSchema, PgTable, PgTransaction } from 'drizzle-orm/pg-core';
2
+ import type { AnyPgColumnBuilder, PgAsyncTransaction, PgSchema, PgTable } from 'drizzle-orm/pg-core';
3
3
  import type { DrizzleConnection } from './database';
4
4
  /**
5
5
  * Returns the Drizzle `PgSchema` for the given tenant, creating it if needed.
@@ -27,7 +27,7 @@ export declare const registerTenantSchema: (tenant: string) => PgSchema;
27
27
  * @param columns The column definitions.
28
28
  * @returns A tenant-scoped `PgTable`.
29
29
  */
30
- export declare const wrapTenantSchema: <TTableName extends string, TColumnsMap extends Record<string, PgColumnBuilderBase>>(tenant: string, tableName: TTableName, columns: TColumnsMap) => PgTable;
30
+ export declare const wrapTenantSchema: <TTableName extends string, TColumnsMap extends Record<string, AnyPgColumnBuilder>>(tenant: string, tableName: TTableName, columns: TColumnsMap) => PgTable;
31
31
  /**
32
32
  * Executes a callback within an RLS-scoped transaction for the given tenant.
33
33
  * @author Axel Nana <axel.nana@workbud.com>
@@ -36,7 +36,7 @@ export declare const wrapTenantSchema: <TTableName extends string, TColumnsMap e
36
36
  * @param connectionName The database connection name.
37
37
  * @returns The return value of the callback.
38
38
  */
39
- export declare const withRLS: <T>(tenant: string | null, callback: (tx: PgTransaction<any, any, any>) => Promise<T>, connectionName?: string) => Promise<T>;
39
+ export declare const withRLS: <T>(tenant: string | null, callback: (tx: PgAsyncTransaction<any, any>) => Promise<T>, connectionName?: string) => Promise<T>;
40
40
  /**
41
41
  * Sets the current tenant on a persistent database connection (session-level).
42
42
  * @author Axel Nana <axel.nana@workbud.com>
@@ -0,0 +1 @@
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elysiumjs/mnemosyne-drizzle",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Drizzle ORM driver for @elysiumjs/mnemosyne",
6
6
  "private": false,
@@ -13,9 +13,8 @@
13
13
  "scripts": {
14
14
  "format": "prettier --write \"./src/**/*.ts\"",
15
15
  "clean": "rimraf dist",
16
- "build:js": "bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify",
17
- "build:ts": "tsc",
18
- "build": "bun run clean && bun run build:js && bun run build:ts",
16
+ "build": "bun run clean && tsc && bun run build:minify",
17
+ "build:minify": "bun scripts/minify.ts",
19
18
  "typecheck": "tsc --noEmit",
20
19
  "prepublishOnly": "bun run build"
21
20
  },
@@ -31,14 +30,17 @@
31
30
  "types": "dist/index.d.ts",
32
31
  "sideEffects": false,
33
32
  "peerDependencies": {
34
- "@elysiumjs/core": "^0.7.0",
35
- "@elysiumjs/mnemosyne": "^0.1.1"
33
+ "@elysiumjs/core": "^0.8.0",
34
+ "@elysiumjs/mnemosyne": "^0.2.0"
36
35
  },
37
36
  "dependencies": {
38
- "drizzle-orm": "^0.45.1"
37
+ "drizzle-orm": "^1.0.0-rc.5",
38
+ "prompts": "^2.4.2",
39
+ "radash": "^12.1.1"
39
40
  },
40
41
  "devDependencies": {
41
- "@elysiumjs/core": "0.7.0",
42
- "@elysiumjs/mnemosyne": "0.1.1"
42
+ "@types/prompts": "^2.4.9",
43
+ "@elysiumjs/core": "0.8.0",
44
+ "@elysiumjs/mnemosyne": "0.2.0"
43
45
  }
44
46
  }
@@ -0,0 +1,10 @@
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
+ }
@@ -0,0 +1,101 @@
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
+ }
@@ -0,0 +1,160 @@
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 CHANGED
@@ -14,9 +14,10 @@
14
14
 
15
15
  import type { CacheInterface, DatabaseCacheConfig, DatabaseDriver } from '@elysiumjs/mnemosyne';
16
16
  import type { DrizzleConfig } from 'drizzle-orm';
17
- import type { BunSQLDatabase } from 'drizzle-orm/bun-sql';
17
+ import type { BunSQLDatabase } from 'drizzle-orm/bun-sql/postgres';
18
18
  import type { CacheConfig } from 'drizzle-orm/cache/core/types';
19
19
 
20
+ import { Service, ServiceNotReadyError } from '@elysiumjs/core';
20
21
  import { AbstractDatabase, createDatabaseCacheStorage } from '@elysiumjs/mnemosyne';
21
22
  import { getTableName, is, Table } from 'drizzle-orm';
22
23
  import { drizzle } from 'drizzle-orm/bun-sql';
@@ -61,7 +62,8 @@ export type DrizzleConnection = BunSQLDatabase<Record<string, never>> & { $clien
61
62
  * @author Axel Nana <axel.nana@workbud.com>
62
63
  */
63
64
  export class DrizzleDatabaseCache extends DrizzleCache {
64
- #cache: Omit<CacheInterface, 'tags'>;
65
+ #storageEngine: 'redis' | 'memory';
66
+ #cache?: Omit<CacheInterface, 'tags'>;
65
67
  private usedTablesPerKey: Record<string, string[]> = {};
66
68
 
67
69
  constructor(
@@ -70,7 +72,23 @@ export class DrizzleDatabaseCache extends DrizzleCache {
70
72
  storageEngine: 'redis' | 'memory' = 'redis'
71
73
  ) {
72
74
  super();
73
- this.#cache = createDatabaseCacheStorage({ storage: storageEngine });
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;
74
92
  }
75
93
 
76
94
  public override strategy(): 'explicit' | 'all' {
@@ -78,7 +96,7 @@ export class DrizzleDatabaseCache extends DrizzleCache {
78
96
  }
79
97
 
80
98
  public override async get(key: string): Promise<any[] | undefined> {
81
- return (await this.#cache.get<any[]>(key)) ?? undefined;
99
+ return (await (await this.#storage()).get<any[]>(key)) ?? undefined;
82
100
  }
83
101
 
84
102
  public override async put(
@@ -88,7 +106,7 @@ export class DrizzleDatabaseCache extends DrizzleCache {
88
106
  _isTag: boolean,
89
107
  config?: CacheConfig
90
108
  ): Promise<void> {
91
- await this.#cache.set(key, response, (config?.ex ?? this.globalTtl) * 1000);
109
+ await (await this.#storage()).set(key, response, (config?.ex ?? this.globalTtl) * 1000);
92
110
  for (const table of tables) {
93
111
  const keys = this.usedTablesPerKey[table];
94
112
  if (keys === undefined) {
@@ -118,8 +136,9 @@ export class DrizzleDatabaseCache extends DrizzleCache {
118
136
  }
119
137
 
120
138
  if (keysToDelete.size > 0 || tagsArray.length > 0) {
121
- await this.#cache.mdel(tagsArray);
122
- await this.#cache.mdel([...keysToDelete]);
139
+ const cache = await this.#storage();
140
+ await cache.mdel(tagsArray);
141
+ await cache.mdel([...keysToDelete]);
123
142
  for (const table of tablesArray) {
124
143
  const tableName = is(table, Table) ? getTableName(table as Table) : (table as string);
125
144
  this.usedTablesPerKey[tableName] = [];
@@ -141,6 +160,17 @@ export class DrizzleDatabaseCache extends DrizzleCache {
141
160
  * @author Axel Nana <axel.nana@workbud.com>
142
161
  */
143
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
+
144
174
  protected driver: DatabaseDriver<DrizzleConnection, DrizzleConnectionProps> = {
145
175
  createConnection(config) {
146
176
  const cache =