@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.
- package/.omc/state/agent-replay-3920b579-6d8e-4f5f-924a-319202936685.jsonl +7 -0
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/mission-state.json +93 -0
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/subagent-tracking-state.json +61 -0
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +40 -0
- package/README.md +51 -0
- package/dist/commands/migration_generate.command.d.ts +11 -0
- package/dist/commands/migration_generate.command.js +1 -0
- package/dist/commands/migration_run.command.d.ts +13 -0
- package/dist/commands/migration_run.command.js +15 -0
- package/dist/database.d.ts +9 -1
- package/dist/database.js +1 -0
- package/dist/drizzle.module.d.ts +40 -0
- package/dist/drizzle.module.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -2
- package/dist/migrations.d.ts +33 -0
- package/dist/migrations.js +1 -0
- package/dist/model.d.ts +5 -5
- package/dist/model.js +1 -0
- package/dist/repository.js +1 -0
- package/dist/table.d.ts +47 -123
- package/dist/table.js +1 -0
- package/dist/tenancy.d.ts +3 -3
- package/dist/tenancy.js +1 -0
- package/package.json +11 -9
- package/scripts/minify.ts +10 -0
- package/src/commands/migration_generate.command.ts +101 -0
- package/src/commands/migration_run.command.ts +160 -0
- package/src/database.ts +37 -7
- package/src/drizzle.module.ts +97 -0
- package/src/index.ts +1 -31
- package/src/migrations.ts +100 -0
- package/src/model.ts +18 -11
- package/src/table.ts +4 -4
- package/src/tenancy.ts +5 -5
- package/tests/drizzle.module.test.ts +201 -0
- package/tests/migrations.test.ts +123 -0
- package/tests/setup.ts +0 -10
- package/tsconfig.json +2 -2
- package/.turbo/turbo-typecheck.log +0 -1
- package/coverage/.lcov.info.01ffde0d9f956dce.tmp +0 -14
- package/coverage/.lcov.info.2f80a4def3a82099.tmp +0 -343
- package/coverage/.lcov.info.560a2daaba0934cd.tmp +0 -366
- package/coverage/.lcov.info.7162c9b788dc0bc6.tmp +0 -366
- package/coverage/.lcov.info.b57c0b452c3901e5.tmp +0 -17
- package/coverage/.lcov.info.c47de8626f331696.tmp +0 -17
- package/coverage/.lcov.info.c710a42b25b08349.tmp +0 -343
- package/coverage/.lcov.info.dbc8f065467fc8a2.tmp +0 -287
- package/coverage/.lcov.info.de180a43301f82c0.tmp +0 -35
- package/coverage/.lcov.info.df18163ab929ab1b.tmp +0 -357
- package/coverage/.lcov.info.febf711c192f5c0c.tmp +0 -17
- package/coverage/lcov.info +0 -357
- package/tests/database.test.ts +0 -198
package/dist/table.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
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,
|
|
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,
|
|
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").
|
|
24
|
-
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:
|
|
29
|
-
|
|
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 :
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} extends infer T_1 ? T_1 extends
|
|
47
|
-
|
|
48
|
-
} ? T_1 extends {
|
|
49
|
-
|
|
50
|
-
} ?
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
} ?
|
|
63
|
-
|
|
64
|
-
} ?
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
hasRuntimeDefault:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 {
|
|
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,
|
|
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:
|
|
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>
|
package/dist/tenancy.js
ADDED
|
@@ -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.
|
|
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
|
|
17
|
-
"build:
|
|
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.
|
|
35
|
-
"@elysiumjs/mnemosyne": "^0.
|
|
33
|
+
"@elysiumjs/core": "^0.8.0",
|
|
34
|
+
"@elysiumjs/mnemosyne": "^0.2.0"
|
|
36
35
|
},
|
|
37
36
|
"dependencies": {
|
|
38
|
-
"drizzle-orm": "^0.
|
|
37
|
+
"drizzle-orm": "^1.0.0-rc.5",
|
|
38
|
+
"prompts": "^2.4.2",
|
|
39
|
+
"radash": "^12.1.1"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@elysiumjs/
|
|
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
|
-
#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
122
|
-
await
|
|
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 =
|