@elysiumjs/mnemosyne-drizzle 0.1.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/.turbo/turbo-build.log +9 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/coverage/.lcov.info.01ffde0d9f956dce.tmp +14 -0
- package/coverage/.lcov.info.2f80a4def3a82099.tmp +343 -0
- package/coverage/.lcov.info.560a2daaba0934cd.tmp +366 -0
- package/coverage/.lcov.info.7162c9b788dc0bc6.tmp +366 -0
- package/coverage/.lcov.info.b57c0b452c3901e5.tmp +17 -0
- package/coverage/.lcov.info.c47de8626f331696.tmp +17 -0
- package/coverage/.lcov.info.c710a42b25b08349.tmp +343 -0
- package/coverage/.lcov.info.dbc8f065467fc8a2.tmp +287 -0
- package/coverage/.lcov.info.de180a43301f82c0.tmp +35 -0
- package/coverage/.lcov.info.df18163ab929ab1b.tmp +357 -0
- package/coverage/.lcov.info.febf711c192f5c0c.tmp +17 -0
- package/coverage/lcov.info +357 -0
- package/dist/database.d.ts +71 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/model.d.ts +61 -0
- package/dist/repository.d.ts +35 -0
- package/dist/table.d.ts +145 -0
- package/dist/tenancy.d.ts +88 -0
- package/package.json +38 -0
- package/src/database.ts +175 -0
- package/src/index.ts +69 -0
- package/src/model.ts +171 -0
- package/src/repository.ts +113 -0
- package/src/table.ts +70 -0
- package/src/tenancy.ts +249 -0
- package/tests/database.test.ts +198 -0
- package/tests/model.test.ts +362 -0
- package/tests/setup.ts +10 -0
- package/tests/table.test.ts +72 -0
- package/tests/tenancy.test.ts +212 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { TenancyStrategy } from '@elysiumjs/mnemosyne';
|
|
2
|
+
import type { PgColumnBuilderBase, PgSchema, PgTable, PgTransaction } from 'drizzle-orm/pg-core';
|
|
3
|
+
import type { DrizzleConnection } from './database';
|
|
4
|
+
/**
|
|
5
|
+
* Returns the Drizzle `PgSchema` for the given tenant, creating it if needed.
|
|
6
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
7
|
+
* @param tenant The tenant identifier.
|
|
8
|
+
* @returns The tenant's PostgreSQL schema.
|
|
9
|
+
*/
|
|
10
|
+
export declare const getTenantSchema: (tenant: string) => PgSchema;
|
|
11
|
+
/**
|
|
12
|
+
* Registers a new tenant schema in the registry.
|
|
13
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
14
|
+
* @param tenant The tenant identifier.
|
|
15
|
+
* @returns The newly registered (or existing) `PgSchema`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const registerTenantSchema: (tenant: string) => PgSchema;
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a table definition in a tenant-specific schema.
|
|
20
|
+
*
|
|
21
|
+
* For the `'public'` tenant, returns a standard `pgTable`.
|
|
22
|
+
* For other tenants, creates a schema-qualified table and caches it.
|
|
23
|
+
*
|
|
24
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
25
|
+
* @param tenant The tenant identifier.
|
|
26
|
+
* @param tableName The table name.
|
|
27
|
+
* @param columns The column definitions.
|
|
28
|
+
* @returns A tenant-scoped `PgTable`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const wrapTenantSchema: <TTableName extends string, TColumnsMap extends Record<string, PgColumnBuilderBase>>(tenant: string, tableName: TTableName, columns: TColumnsMap) => PgTable;
|
|
31
|
+
/**
|
|
32
|
+
* Executes a callback within an RLS-scoped transaction for the given tenant.
|
|
33
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
34
|
+
* @param tenant The tenant identifier (or `null` for 'public').
|
|
35
|
+
* @param callback The function to execute within the transaction.
|
|
36
|
+
* @param connectionName The database connection name.
|
|
37
|
+
* @returns The return value of the callback.
|
|
38
|
+
*/
|
|
39
|
+
export declare const withRLS: <T>(tenant: string | null, callback: (tx: PgTransaction<any, any, any>) => Promise<T>, connectionName?: string) => Promise<T>;
|
|
40
|
+
/**
|
|
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.
|
|
49
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
50
|
+
* @param connectionName The database connection name.
|
|
51
|
+
* @returns The current tenant identifier, or `null`.
|
|
52
|
+
*/
|
|
53
|
+
export declare const getSessionTenant: (connectionName?: string) => Promise<string | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Creates an RLS policy for tenant isolation on a table.
|
|
56
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
57
|
+
* @param tableName The table name.
|
|
58
|
+
* @param columnName The tenant column name (defaults to config or 'tenant_id').
|
|
59
|
+
* @param policyName The policy name (defaults to `{tableName}_tenant_isolation`).
|
|
60
|
+
* @returns A Drizzle `PgPolicy` instance.
|
|
61
|
+
*/
|
|
62
|
+
export declare const createRLSPolicy: (tableName: string, columnName?: string, policyName?: string) => import("drizzle-orm/pg-core").PgPolicy;
|
|
63
|
+
/**
|
|
64
|
+
* Schema-based tenancy strategy for Drizzle.
|
|
65
|
+
*
|
|
66
|
+
* Each tenant gets its own PostgreSQL schema. Table resolution maps
|
|
67
|
+
* the base table to a schema-qualified equivalent.
|
|
68
|
+
*
|
|
69
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
70
|
+
*/
|
|
71
|
+
export declare class DrizzleSchemaTenancy implements TenancyStrategy<PgTable, DrizzleConnection> {
|
|
72
|
+
readonly mode = "schema";
|
|
73
|
+
resolveTable(table: PgTable, tenant: string): PgTable;
|
|
74
|
+
withIsolation<T>(_connection: DrizzleConnection, _tenant: string, callback: () => Promise<T>): Promise<T>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Row-Level Security tenancy strategy for Drizzle.
|
|
78
|
+
*
|
|
79
|
+
* All tenants share the same schema. Isolation is achieved via
|
|
80
|
+
* PostgreSQL RLS policies and a session variable set in each transaction.
|
|
81
|
+
*
|
|
82
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
83
|
+
*/
|
|
84
|
+
export declare class DrizzleRLSTenancy implements TenancyStrategy<PgTable, DrizzleConnection> {
|
|
85
|
+
readonly mode = "rls";
|
|
86
|
+
resolveTable(table: PgTable, _tenant: string): PgTable;
|
|
87
|
+
withIsolation<T>(connection: DrizzleConnection, tenant: string, callback: () => Promise<T>): Promise<T>;
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elysiumjs/mnemosyne-drizzle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "Drizzle ORM driver for @elysiumjs/mnemosyne",
|
|
6
|
+
"private": false,
|
|
7
|
+
"author": "Workbud Technologies Inc.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/workbud/elysium"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/workbud/elysium",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"format": "prettier --write \"./src/**/*.ts\"",
|
|
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",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"prepublishOnly": "bun run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["elysium", "database", "orm", "drizzle", "postgresql"],
|
|
23
|
+
"module": "dist/index.js",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@elysiumjs/core": "^0.7.0",
|
|
29
|
+
"@elysiumjs/mnemosyne": "^0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"drizzle-orm": "^0.44.6"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@elysiumjs/core": "0.7.0",
|
|
36
|
+
"@elysiumjs/mnemosyne": "0.1.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/database.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import type { CacheInterface, DatabaseCacheConfig, DatabaseDriver } from '@elysiumjs/mnemosyne';
|
|
16
|
+
import type { DrizzleConfig } from 'drizzle-orm';
|
|
17
|
+
import type { BunSQLDatabase } from 'drizzle-orm/bun-sql';
|
|
18
|
+
import type { CacheConfig } from 'drizzle-orm/cache/core/types';
|
|
19
|
+
|
|
20
|
+
import { AbstractDatabase, createDatabaseCacheStorage } from '@elysiumjs/mnemosyne';
|
|
21
|
+
import { getTableName, is, Table } from 'drizzle-orm';
|
|
22
|
+
import { drizzle } from 'drizzle-orm/bun-sql';
|
|
23
|
+
import { Cache as DrizzleCache } from 'drizzle-orm/cache/core';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Types
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Connection configuration for Drizzle ORM.
|
|
31
|
+
*
|
|
32
|
+
* Extends `DrizzleConfig` with connection details and optional caching.
|
|
33
|
+
*
|
|
34
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
35
|
+
* @template TSchema The Drizzle schema type.
|
|
36
|
+
*/
|
|
37
|
+
export type DrizzleConnectionProps<
|
|
38
|
+
TSchema extends Record<string, unknown> = Record<string, never>
|
|
39
|
+
> = Omit<DrizzleConfig<TSchema>, 'cache'> &
|
|
40
|
+
({ connection: string | ({ url?: string } & Bun.SQLOptions) } | { client: Bun.SQL }) & {
|
|
41
|
+
cache?: true | DatabaseCacheConfig;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A Drizzle database connection backed by Bun's SQL driver.
|
|
46
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
47
|
+
*/
|
|
48
|
+
export type DrizzleConnection = BunSQLDatabase<Record<string, never>> & { $client: Bun.SQL };
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// DrizzleDatabaseCache
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Drizzle query cache backed by the framework's cache storage.
|
|
56
|
+
*
|
|
57
|
+
* Extends the Drizzle `Cache` base class and implements the
|
|
58
|
+
* framework's `DatabaseCacheStrategy` interface so that query
|
|
59
|
+
* results can be transparently cached and invalidated.
|
|
60
|
+
*
|
|
61
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
62
|
+
*/
|
|
63
|
+
export class DrizzleDatabaseCache extends DrizzleCache {
|
|
64
|
+
#cache: Omit<CacheInterface, 'tags'>;
|
|
65
|
+
private usedTablesPerKey: Record<string, string[]> = {};
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly cacheStrategy: 'explicit' | 'all' = 'explicit',
|
|
69
|
+
private readonly globalTtl: number = 1,
|
|
70
|
+
storageEngine: 'redis' | 'memory' = 'redis'
|
|
71
|
+
) {
|
|
72
|
+
super();
|
|
73
|
+
this.#cache = createDatabaseCacheStorage({ storage: storageEngine });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public override strategy(): 'explicit' | 'all' {
|
|
77
|
+
return this.cacheStrategy;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public override async get(key: string): Promise<any[] | undefined> {
|
|
81
|
+
return (await this.#cache.get<any[]>(key)) ?? undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public override async put(
|
|
85
|
+
key: string,
|
|
86
|
+
response: any,
|
|
87
|
+
tables: string[],
|
|
88
|
+
_isTag: boolean,
|
|
89
|
+
config?: CacheConfig
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
await this.#cache.set(key, response, (config?.ex ?? this.globalTtl) * 1000);
|
|
92
|
+
for (const table of tables) {
|
|
93
|
+
const keys = this.usedTablesPerKey[table];
|
|
94
|
+
if (keys === undefined) {
|
|
95
|
+
this.usedTablesPerKey[table] = [key];
|
|
96
|
+
} else {
|
|
97
|
+
keys.push(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public override async onMutate(params: {
|
|
103
|
+
tags?: string | string[];
|
|
104
|
+
tables?: string | string[] | Table<any> | Table<any>[];
|
|
105
|
+
}): Promise<void> {
|
|
106
|
+
const tagsArray = params.tags ? (Array.isArray(params.tags) ? params.tags : [params.tags]) : [];
|
|
107
|
+
const tablesArray = params.tables
|
|
108
|
+
? Array.isArray(params.tables)
|
|
109
|
+
? params.tables
|
|
110
|
+
: [params.tables]
|
|
111
|
+
: [];
|
|
112
|
+
const keysToDelete = new Set<string>();
|
|
113
|
+
|
|
114
|
+
for (const table of tablesArray) {
|
|
115
|
+
const tableName = is(table, Table) ? getTableName(table as Table) : (table as string);
|
|
116
|
+
const keys = this.usedTablesPerKey[tableName] ?? [];
|
|
117
|
+
for (const key of keys) keysToDelete.add(key);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (keysToDelete.size > 0 || tagsArray.length > 0) {
|
|
121
|
+
await this.#cache.mdel(tagsArray);
|
|
122
|
+
await this.#cache.mdel([...keysToDelete]);
|
|
123
|
+
for (const table of tablesArray) {
|
|
124
|
+
const tableName = is(table, Table) ? getTableName(table as Table) : (table as string);
|
|
125
|
+
this.usedTablesPerKey[tableName] = [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// DrizzleDatabase
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Drizzle-specific database manager.
|
|
137
|
+
*
|
|
138
|
+
* Implements the abstract database class with a driver that creates
|
|
139
|
+
* Drizzle connections using Bun's built-in SQL client.
|
|
140
|
+
*
|
|
141
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
142
|
+
*/
|
|
143
|
+
class DrizzleDatabase extends AbstractDatabase<DrizzleConnection, DrizzleConnectionProps> {
|
|
144
|
+
protected driver: DatabaseDriver<DrizzleConnection, DrizzleConnectionProps> = {
|
|
145
|
+
createConnection(config) {
|
|
146
|
+
const cache =
|
|
147
|
+
config.cache === undefined
|
|
148
|
+
? undefined
|
|
149
|
+
: config.cache === true
|
|
150
|
+
? new DrizzleDatabaseCache()
|
|
151
|
+
: config.cache.enabled !== false
|
|
152
|
+
? new DrizzleDatabaseCache(
|
|
153
|
+
config.cache.strategy,
|
|
154
|
+
config.cache.ttl,
|
|
155
|
+
config.cache.storage
|
|
156
|
+
)
|
|
157
|
+
: undefined;
|
|
158
|
+
return drizzle({ ...config, cache }) as DrizzleConnection;
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
withTransaction(connection, callback) {
|
|
162
|
+
return (connection as any).transaction((tx: any) => callback(tx));
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
getRawClient(connection) {
|
|
166
|
+
return connection.$client;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Singleton Drizzle database manager.
|
|
173
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
174
|
+
*/
|
|
175
|
+
export const Database = new DrizzleDatabase();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import type { MnemosyneConfig } from '@elysiumjs/mnemosyne';
|
|
16
|
+
import type { DrizzleConnectionProps } from './database';
|
|
17
|
+
|
|
18
|
+
import { Application, Event } from '@elysiumjs/core';
|
|
19
|
+
import { registerTenancyStrategy } from '@elysiumjs/mnemosyne';
|
|
20
|
+
|
|
21
|
+
import { Database } from './database';
|
|
22
|
+
import { DrizzleRLSTenancy, DrizzleSchemaTenancy } from './tenancy';
|
|
23
|
+
|
|
24
|
+
// Re-export drizzle-orm/pg-core with enhanced pgTable
|
|
25
|
+
export * from 'drizzle-orm/pg-core';
|
|
26
|
+
export { pgTable, getTableBuilders } from './table';
|
|
27
|
+
|
|
28
|
+
// Re-exports
|
|
29
|
+
export { Database, DrizzleDatabaseCache } from './database';
|
|
30
|
+
export type { DrizzleConnectionProps, DrizzleConnection } from './database';
|
|
31
|
+
export {
|
|
32
|
+
DrizzleModel,
|
|
33
|
+
createSchemaFromDrizzle,
|
|
34
|
+
drizzleAdapter,
|
|
35
|
+
type DrizzleModelOptions
|
|
36
|
+
} from './model';
|
|
37
|
+
export { DrizzleRepository } from './repository';
|
|
38
|
+
export {
|
|
39
|
+
DrizzleSchemaTenancy,
|
|
40
|
+
DrizzleRLSTenancy,
|
|
41
|
+
wrapTenantSchema,
|
|
42
|
+
registerTenantSchema,
|
|
43
|
+
getTenantSchema,
|
|
44
|
+
withRLS,
|
|
45
|
+
setConnectionTenant,
|
|
46
|
+
getSessionTenant,
|
|
47
|
+
createRLSPolicy
|
|
48
|
+
} from './tenancy';
|
|
49
|
+
|
|
50
|
+
// Auto-initialize on app launch
|
|
51
|
+
Event.once('elysium:app:launched', () => {
|
|
52
|
+
const app = Application.instance;
|
|
53
|
+
const config = app.getConfig<MnemosyneConfig<DrizzleConnectionProps>>('elysium:mnemosyne' as any);
|
|
54
|
+
|
|
55
|
+
if (config?.database) {
|
|
56
|
+
for (const [name, props] of Object.entries(config.database.connections)) {
|
|
57
|
+
Database.registerConnection(name, props);
|
|
58
|
+
}
|
|
59
|
+
if (Database.connectionExists(config.database.default)) {
|
|
60
|
+
Database.setDefaultConnection(config.database.default);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config?.tenancy) {
|
|
65
|
+
const strategy =
|
|
66
|
+
config.tenancy.mode === 'rls' ? new DrizzleRLSTenancy() : new DrizzleSchemaTenancy();
|
|
67
|
+
registerTenancyStrategy(strategy);
|
|
68
|
+
}
|
|
69
|
+
});
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import type { ColumnMetadata, ModelAdapter } from '@elysiumjs/mnemosyne';
|
|
16
|
+
import type { PgColumn, PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
17
|
+
|
|
18
|
+
import { AbstractModel, createSchemaFromModel } from '@elysiumjs/mnemosyne';
|
|
19
|
+
import { getTableConfig } from 'drizzle-orm/pg-core';
|
|
20
|
+
|
|
21
|
+
import { getTableBuilders } from './table';
|
|
22
|
+
import { wrapTenantSchema } from './tenancy';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Column Type Mapping
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps a Drizzle column to a normalized data type.
|
|
30
|
+
* @param col The Drizzle column to inspect.
|
|
31
|
+
* @returns A normalized data type string.
|
|
32
|
+
*/
|
|
33
|
+
const mapDrizzleDataType = (col: PgColumn): ColumnMetadata['dataType'] => {
|
|
34
|
+
if (col.columnType === 'PgUUID') return 'uuid';
|
|
35
|
+
switch (col.dataType) {
|
|
36
|
+
case 'string':
|
|
37
|
+
return 'string';
|
|
38
|
+
case 'number':
|
|
39
|
+
return 'number';
|
|
40
|
+
case 'boolean':
|
|
41
|
+
return 'boolean';
|
|
42
|
+
case 'array':
|
|
43
|
+
return 'array';
|
|
44
|
+
case 'json':
|
|
45
|
+
return 'json';
|
|
46
|
+
case 'date':
|
|
47
|
+
return 'date';
|
|
48
|
+
case 'bigint':
|
|
49
|
+
return 'bigint';
|
|
50
|
+
case 'buffer':
|
|
51
|
+
return 'buffer';
|
|
52
|
+
default:
|
|
53
|
+
return 'string';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Drizzle Model Adapter
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Drizzle-specific model adapter.
|
|
63
|
+
*
|
|
64
|
+
* Bridges Drizzle's `PgTableWithColumns` to the framework's
|
|
65
|
+
* normalized model metadata.
|
|
66
|
+
*
|
|
67
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
68
|
+
*/
|
|
69
|
+
export const drizzleAdapter: ModelAdapter<PgTableWithColumns<any>> = {
|
|
70
|
+
getTableName(table) {
|
|
71
|
+
return getTableConfig(table).name;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
getColumns(table): ColumnMetadata[] {
|
|
75
|
+
const config = getTableConfig(table);
|
|
76
|
+
return config.columns.map((col: PgColumn) => ({
|
|
77
|
+
name: col.name,
|
|
78
|
+
dataType: mapDrizzleDataType(col),
|
|
79
|
+
nullable: !col.notNull,
|
|
80
|
+
hasDefault: col.hasDefault,
|
|
81
|
+
isPrimaryKey:
|
|
82
|
+
col.primary || config.primaryKeys.some((pk) => pk.columns.some((c) => c.name === col.name))
|
|
83
|
+
}));
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
createTenantTable(table, tenant) {
|
|
87
|
+
const builders = getTableBuilders(table);
|
|
88
|
+
if (!builders) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`No column builders registered for table. ` +
|
|
91
|
+
`Use pgTable() from '@elysiumjs/mnemosyne-drizzle' for tenant support.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const config = getTableConfig(table);
|
|
95
|
+
return wrapTenantSchema(tenant, config.name, builders) as PgTableWithColumns<any>;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Schema Helper
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates Elysia validation schemas from a Drizzle table.
|
|
105
|
+
*
|
|
106
|
+
* This is a convenience wrapper around `createSchemaFromModel` that
|
|
107
|
+
* automatically extracts column metadata via the `drizzleAdapter`.
|
|
108
|
+
*
|
|
109
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
110
|
+
* @param table The Drizzle table to generate schemas for.
|
|
111
|
+
* @param opts Schema generation options.
|
|
112
|
+
* @returns An Elysia validation schema.
|
|
113
|
+
*/
|
|
114
|
+
export const createSchemaFromDrizzle = (
|
|
115
|
+
table: PgTableWithColumns<any>,
|
|
116
|
+
opts?: { mode?: 'create' | 'update' | 'select' }
|
|
117
|
+
) => {
|
|
118
|
+
return createSchemaFromModel(drizzleAdapter.getColumns(table), opts);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// DrizzleModel Options
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Options for creating a DrizzleModel.
|
|
127
|
+
*/
|
|
128
|
+
export interface DrizzleModelOptions {
|
|
129
|
+
/**
|
|
130
|
+
* Enable tenant schema support for this model.
|
|
131
|
+
* @default false
|
|
132
|
+
*/
|
|
133
|
+
supportTenancy?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// DrizzleModel Function
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates a model class backed by a Drizzle pgTable.
|
|
142
|
+
*
|
|
143
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
144
|
+
* @param table The Drizzle table (created with enhanced pgTable).
|
|
145
|
+
* @param options Model configuration options.
|
|
146
|
+
* @returns A model class with Drizzle-backed static metadata.
|
|
147
|
+
*/
|
|
148
|
+
export const DrizzleModel = <TTable extends PgTableWithColumns<any>>(
|
|
149
|
+
table: TTable,
|
|
150
|
+
options?: DrizzleModelOptions
|
|
151
|
+
) => {
|
|
152
|
+
const config = getTableConfig(table);
|
|
153
|
+
const builders = getTableBuilders(table);
|
|
154
|
+
|
|
155
|
+
type TSelect = TTable['$inferSelect'];
|
|
156
|
+
type TInsert = TTable['$inferInsert'];
|
|
157
|
+
|
|
158
|
+
const Base = AbstractModel(config.name, builders ?? {}, drizzleAdapter, table);
|
|
159
|
+
|
|
160
|
+
// Override supportTenancy if specified
|
|
161
|
+
if (options?.supportTenancy !== undefined) {
|
|
162
|
+
(Base as any).supportTenancy = options.supportTenancy;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Base as typeof Base & {
|
|
166
|
+
readonly $inferSelect: TSelect;
|
|
167
|
+
readonly $inferInsert: TInsert;
|
|
168
|
+
readonly $inferUpdate: Partial<TInsert>;
|
|
169
|
+
readonly table: TTable;
|
|
170
|
+
};
|
|
171
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import type { IdType, ModelClass } from '@elysiumjs/mnemosyne';
|
|
16
|
+
import type { PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
17
|
+
import type { DrizzleConnection } from './database';
|
|
18
|
+
|
|
19
|
+
import { AbstractRepository } from '@elysiumjs/mnemosyne';
|
|
20
|
+
import { eq } from 'drizzle-orm';
|
|
21
|
+
|
|
22
|
+
import { Database } from './database';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// DrizzleRepository Mixin
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Drizzle-specific repository mixin with concrete CRUD operations.
|
|
30
|
+
*
|
|
31
|
+
* Extends the abstract repository with Drizzle query builder calls
|
|
32
|
+
* for all standard CRUD methods.
|
|
33
|
+
*
|
|
34
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
35
|
+
* @template TModel The model class type.
|
|
36
|
+
* @param model The model class to create the repository for.
|
|
37
|
+
* @returns A concrete repository class with CRUD methods.
|
|
38
|
+
*/
|
|
39
|
+
export const DrizzleRepository = <
|
|
40
|
+
TModel extends ModelClass<any, any, any, PgTableWithColumns<any>>
|
|
41
|
+
>(
|
|
42
|
+
model: TModel
|
|
43
|
+
) => {
|
|
44
|
+
type TSelect = TModel['$inferSelect'];
|
|
45
|
+
type TInsert = TModel['$inferInsert'];
|
|
46
|
+
type TUpdate = TModel['$inferUpdate'];
|
|
47
|
+
|
|
48
|
+
class R extends AbstractRepository<TModel, DrizzleConnection>(model, Database) {
|
|
49
|
+
public async all(): Promise<TSelect[]> {
|
|
50
|
+
return await this.db.select().from(model.table);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async paginate(
|
|
54
|
+
count: number,
|
|
55
|
+
page: number = 1
|
|
56
|
+
): Promise<{ page: number; data: TSelect[]; total: number }> {
|
|
57
|
+
const total = await this.db.$count(model.table);
|
|
58
|
+
const offset = Math.max(page - 1, 0) * count;
|
|
59
|
+
return {
|
|
60
|
+
page,
|
|
61
|
+
total,
|
|
62
|
+
data: await this.db.select().from(model.table).offset(offset).limit(count)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async find(id: IdType): Promise<TSelect | null> {
|
|
67
|
+
const [row] = await this.db.select().from(model.table).where(eq(model.table.id, id));
|
|
68
|
+
return (row ?? null) as TSelect | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async findBy<TKey extends keyof TSelect>(
|
|
72
|
+
column: TKey,
|
|
73
|
+
value: TSelect[TKey]
|
|
74
|
+
): Promise<TSelect | null> {
|
|
75
|
+
const [row] = await this.db.select().from(model.table).where(eq(model.table[column], value));
|
|
76
|
+
return (row ?? null) as TSelect | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async insert(data: TInsert): Promise<TSelect> {
|
|
80
|
+
const [row] = await this.db.insert(model.table).values(data).returning();
|
|
81
|
+
return row as TSelect;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public async update(id: IdType, data: TUpdate): Promise<TSelect> {
|
|
85
|
+
const [row] = await this.db
|
|
86
|
+
.update(model.table)
|
|
87
|
+
.set(data)
|
|
88
|
+
.where(eq(model.table.id, id))
|
|
89
|
+
.returning();
|
|
90
|
+
return row as TSelect;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async updateAll(data: TUpdate): Promise<TSelect[]> {
|
|
94
|
+
return await this.db.update(model.table).set(data).returning();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async delete(id: IdType): Promise<TSelect> {
|
|
98
|
+
const [row] = await this.db.delete(model.table).where(eq(model.table.id, id)).returning();
|
|
99
|
+
return row as TSelect;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async deleteAll(): Promise<TSelect[]> {
|
|
103
|
+
return await this.db.delete(model.table).returning();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public async exists(id: IdType): Promise<boolean> {
|
|
107
|
+
const count = await this.db.$count(model.table, eq(model.table.id, id));
|
|
108
|
+
return count > 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return R;
|
|
113
|
+
};
|