@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
package/src/table.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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 { PgColumnBuilderBase, PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
16
|
+
|
|
17
|
+
import { pgTable as drizzlePgTable } from 'drizzle-orm/pg-core';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Column Builder Registry
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maps tables to their original column builder definitions.
|
|
25
|
+
*
|
|
26
|
+
* When `pgTable()` is called, column builders are consumed and turned into
|
|
27
|
+
* built `PgColumn` instances. This registry preserves the original builder
|
|
28
|
+
* objects so that tenant tables can be re-created in different schemas.
|
|
29
|
+
*/
|
|
30
|
+
const tableRegistry = new WeakMap<PgTableWithColumns<any>, Record<string, PgColumnBuilderBase>>();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Retrieves the original column builders for a table.
|
|
34
|
+
* @param table The table created with enhanced pgTable.
|
|
35
|
+
* @returns The original column builder definitions, or `undefined`.
|
|
36
|
+
*/
|
|
37
|
+
export const getTableBuilders = (
|
|
38
|
+
table: PgTableWithColumns<any>
|
|
39
|
+
): Record<string, PgColumnBuilderBase> | undefined => {
|
|
40
|
+
return tableRegistry.get(table);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Enhanced pgTable Function
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a PostgreSQL table with automatic column builder registration.
|
|
49
|
+
*
|
|
50
|
+
* This is an enhanced version of Drizzle's `pgTable` that automatically
|
|
51
|
+
* registers column builders for tenancy support. It has the exact same
|
|
52
|
+
* signature and behavior as the original.
|
|
53
|
+
*
|
|
54
|
+
* @param tableName The name of the database table.
|
|
55
|
+
* @param columns The column definitions.
|
|
56
|
+
* @param extraConfig Optional extra Drizzle table configuration.
|
|
57
|
+
* @returns A Drizzle PostgreSQL table with registered column builders.
|
|
58
|
+
*/
|
|
59
|
+
export const pgTable = <
|
|
60
|
+
TTableName extends string,
|
|
61
|
+
TColumns extends Record<string, PgColumnBuilderBase>
|
|
62
|
+
>(
|
|
63
|
+
tableName: TTableName,
|
|
64
|
+
columns: TColumns,
|
|
65
|
+
extraConfig?: (table: any) => any
|
|
66
|
+
) => {
|
|
67
|
+
const table = drizzlePgTable(tableName, columns, extraConfig);
|
|
68
|
+
tableRegistry.set(table, columns);
|
|
69
|
+
return table;
|
|
70
|
+
};
|
package/src/tenancy.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
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 { TenancyStrategy } from '@elysiumjs/mnemosyne';
|
|
16
|
+
import type { PgColumnBuilderBase, PgSchema, PgTable, PgTransaction } from 'drizzle-orm/pg-core';
|
|
17
|
+
import type { DrizzleConnection } from './database';
|
|
18
|
+
|
|
19
|
+
import { getTenancyConfig } from '@elysiumjs/mnemosyne';
|
|
20
|
+
import { sql } from 'drizzle-orm';
|
|
21
|
+
import { getTableConfig, pgPolicy, pgSchema, pgTable } from 'drizzle-orm/pg-core';
|
|
22
|
+
|
|
23
|
+
import { Database } from './database';
|
|
24
|
+
import { getTableBuilders } from './table';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Internal State
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
const tenantSchemas: Map<string, PgTable> = new Map();
|
|
31
|
+
const schemaRegistry: Map<string, PgSchema> = new Map();
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Schema-based Tenancy Helpers
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the Drizzle `PgSchema` for the given tenant, creating it if needed.
|
|
39
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
40
|
+
* @param tenant The tenant identifier.
|
|
41
|
+
* @returns The tenant's PostgreSQL schema.
|
|
42
|
+
*/
|
|
43
|
+
export const getTenantSchema = (tenant: string): PgSchema => {
|
|
44
|
+
return schemaRegistry.get(tenant) ?? registerTenantSchema(tenant);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registers a new tenant schema in the registry.
|
|
49
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
50
|
+
* @param tenant The tenant identifier.
|
|
51
|
+
* @returns The newly registered (or existing) `PgSchema`.
|
|
52
|
+
*/
|
|
53
|
+
export const registerTenantSchema = (tenant: string): PgSchema => {
|
|
54
|
+
if (schemaRegistry.has(tenant)) {
|
|
55
|
+
return schemaRegistry.get(tenant)!;
|
|
56
|
+
}
|
|
57
|
+
const tenantSchema = pgSchema(tenant);
|
|
58
|
+
schemaRegistry.set(tenant, tenantSchema);
|
|
59
|
+
return tenantSchema;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wraps a table definition in a tenant-specific schema.
|
|
64
|
+
*
|
|
65
|
+
* For the `'public'` tenant, returns a standard `pgTable`.
|
|
66
|
+
* For other tenants, creates a schema-qualified table and caches it.
|
|
67
|
+
*
|
|
68
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
69
|
+
* @param tenant The tenant identifier.
|
|
70
|
+
* @param tableName The table name.
|
|
71
|
+
* @param columns The column definitions.
|
|
72
|
+
* @returns A tenant-scoped `PgTable`.
|
|
73
|
+
*/
|
|
74
|
+
export const wrapTenantSchema = <
|
|
75
|
+
TTableName extends string,
|
|
76
|
+
TColumnsMap extends Record<string, PgColumnBuilderBase>
|
|
77
|
+
>(
|
|
78
|
+
tenant: string,
|
|
79
|
+
tableName: TTableName,
|
|
80
|
+
columns: TColumnsMap
|
|
81
|
+
): PgTable => {
|
|
82
|
+
if (tenant === 'public') {
|
|
83
|
+
return pgTable(tableName, columns);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const fullTableName = `${tenant}.${tableName}`;
|
|
87
|
+
if (tenantSchemas.has(fullTableName)) {
|
|
88
|
+
return tenantSchemas.get(fullTableName)!;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tenantSchema = getTenantSchema(tenant);
|
|
92
|
+
const schemaTable = tenantSchema.table(tableName, columns);
|
|
93
|
+
tenantSchemas.set(fullTableName, schemaTable);
|
|
94
|
+
|
|
95
|
+
return schemaTable;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// RLS-based Tenancy Helpers
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Executes a callback within an RLS-scoped transaction for the given tenant.
|
|
104
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
105
|
+
* @param tenant The tenant identifier (or `null` for 'public').
|
|
106
|
+
* @param callback The function to execute within the transaction.
|
|
107
|
+
* @param connectionName The database connection name.
|
|
108
|
+
* @returns The return value of the callback.
|
|
109
|
+
*/
|
|
110
|
+
export const withRLS = async <T>(
|
|
111
|
+
tenant: string | null,
|
|
112
|
+
callback: (tx: PgTransaction<any, any, any>) => Promise<T>,
|
|
113
|
+
connectionName: string = 'default'
|
|
114
|
+
): Promise<T> => {
|
|
115
|
+
const db = Database.getConnection(connectionName);
|
|
116
|
+
const config = getTenancyConfig();
|
|
117
|
+
const varName = config.rls?.sessionVariable ?? 'app.current_tenant';
|
|
118
|
+
const value = tenant ?? 'public';
|
|
119
|
+
|
|
120
|
+
return (db as any).transaction(async (tx: PgTransaction<any, any, any>) => {
|
|
121
|
+
await tx.execute(sql`SELECT set_config(${varName}, ${value}, true)`);
|
|
122
|
+
return callback(tx);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sets the current tenant on a persistent database connection (session-level).
|
|
128
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
129
|
+
* @param tenant The tenant identifier (or `null` for 'public').
|
|
130
|
+
* @param connectionName The database connection name.
|
|
131
|
+
*/
|
|
132
|
+
export const setConnectionTenant = async (
|
|
133
|
+
tenant: string | null,
|
|
134
|
+
connectionName: string = 'default'
|
|
135
|
+
): Promise<void> => {
|
|
136
|
+
const db = Database.getConnection(connectionName);
|
|
137
|
+
const config = getTenancyConfig();
|
|
138
|
+
const varName = config.rls?.sessionVariable ?? 'app.current_tenant';
|
|
139
|
+
const value = tenant ?? 'public';
|
|
140
|
+
|
|
141
|
+
await (db as any).execute(sql`SELECT set_config(${varName}, ${value}, false)`);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Reads the current tenant from the PostgreSQL session variable.
|
|
146
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
147
|
+
* @param connectionName The database connection name.
|
|
148
|
+
* @returns The current tenant identifier, or `null`.
|
|
149
|
+
*/
|
|
150
|
+
export const getSessionTenant = async (
|
|
151
|
+
connectionName: string = 'default'
|
|
152
|
+
): Promise<string | null> => {
|
|
153
|
+
const db = Database.getConnection(connectionName);
|
|
154
|
+
const config = getTenancyConfig();
|
|
155
|
+
const varName = config.rls?.sessionVariable ?? 'app.current_tenant';
|
|
156
|
+
|
|
157
|
+
const result = await (db as any).execute(sql`SELECT current_setting(${varName}, true) as tenant`);
|
|
158
|
+
return (result as any)?.rows?.[0]?.tenant ?? null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Creates an RLS policy for tenant isolation on a table.
|
|
163
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
164
|
+
* @param tableName The table name.
|
|
165
|
+
* @param columnName The tenant column name (defaults to config or 'tenant_id').
|
|
166
|
+
* @param policyName The policy name (defaults to `{tableName}_tenant_isolation`).
|
|
167
|
+
* @returns A Drizzle `PgPolicy` instance.
|
|
168
|
+
*/
|
|
169
|
+
export const createRLSPolicy = (tableName: string, columnName?: string, policyName?: string) => {
|
|
170
|
+
const config = getTenancyConfig();
|
|
171
|
+
const varName = config.rls?.sessionVariable ?? 'app.current_tenant';
|
|
172
|
+
const column = columnName ?? config.rls?.defaultColumn ?? 'tenant_id';
|
|
173
|
+
const name = policyName ?? `${tableName}_tenant_isolation`;
|
|
174
|
+
|
|
175
|
+
return pgPolicy(name, {
|
|
176
|
+
for: 'all',
|
|
177
|
+
to: 'public',
|
|
178
|
+
using: sql`${sql.identifier(column)} = current_setting(${varName}, true)::text`,
|
|
179
|
+
withCheck: sql`${sql.identifier(column)} = current_setting(${varName}, true)::text`
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// TenancyStrategy Implementations
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Schema-based tenancy strategy for Drizzle.
|
|
189
|
+
*
|
|
190
|
+
* Each tenant gets its own PostgreSQL schema. Table resolution maps
|
|
191
|
+
* the base table to a schema-qualified equivalent.
|
|
192
|
+
*
|
|
193
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
194
|
+
*/
|
|
195
|
+
export class DrizzleSchemaTenancy implements TenancyStrategy<PgTable, DrizzleConnection> {
|
|
196
|
+
readonly mode = 'schema';
|
|
197
|
+
|
|
198
|
+
resolveTable(table: PgTable, tenant: string): PgTable {
|
|
199
|
+
const builders = getTableBuilders(table as any);
|
|
200
|
+
if (!builders) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`No column builders registered for table. ` +
|
|
203
|
+
`Use pgTable() from '@elysiumjs/mnemosyne-drizzle' for tenant support.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const config = getTableConfig(table as any);
|
|
207
|
+
return wrapTenantSchema(tenant, config.name, builders) as PgTable;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async withIsolation<T>(
|
|
211
|
+
_connection: DrizzleConnection,
|
|
212
|
+
_tenant: string,
|
|
213
|
+
callback: () => Promise<T>
|
|
214
|
+
): Promise<T> {
|
|
215
|
+
// Schema-based isolation doesn't need transaction wrapping
|
|
216
|
+
return callback();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Row-Level Security tenancy strategy for Drizzle.
|
|
222
|
+
*
|
|
223
|
+
* All tenants share the same schema. Isolation is achieved via
|
|
224
|
+
* PostgreSQL RLS policies and a session variable set in each transaction.
|
|
225
|
+
*
|
|
226
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
227
|
+
*/
|
|
228
|
+
export class DrizzleRLSTenancy implements TenancyStrategy<PgTable, DrizzleConnection> {
|
|
229
|
+
readonly mode = 'rls';
|
|
230
|
+
|
|
231
|
+
resolveTable(table: PgTable, _tenant: string): PgTable {
|
|
232
|
+
// RLS doesn't modify the table - filtering is handled by policies
|
|
233
|
+
return table;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async withIsolation<T>(
|
|
237
|
+
connection: DrizzleConnection,
|
|
238
|
+
tenant: string,
|
|
239
|
+
callback: () => Promise<T>
|
|
240
|
+
): Promise<T> {
|
|
241
|
+
const config = getTenancyConfig();
|
|
242
|
+
const varName = config.rls?.sessionVariable ?? 'app.current_tenant';
|
|
243
|
+
|
|
244
|
+
return (connection as any).transaction(async (tx: PgTransaction<any, any, any>) => {
|
|
245
|
+
await tx.execute(sql`SELECT set_config(${varName}, ${tenant}, true)`);
|
|
246
|
+
return callback();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
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 { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
import { Application, Service } from '@elysiumjs/core';
|
|
18
|
+
|
|
19
|
+
import { Database } from '../src/database';
|
|
20
|
+
|
|
21
|
+
// Mock drizzle function
|
|
22
|
+
mock.module('drizzle-orm/bun-sql', () => ({
|
|
23
|
+
drizzle: mock(() => ({ mockDrizzleInstance: true }))
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe('DrizzleDatabase', () => {
|
|
27
|
+
// Mock console.error and process.exit to prevent actual console output and process exit
|
|
28
|
+
const originalConsoleError = console.error;
|
|
29
|
+
const originalProcessExit = process.exit;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
// Mock Application.context so that getConnection('default') works
|
|
33
|
+
const mockContext = {
|
|
34
|
+
getStore: mock(() => new Map())
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(Application, 'context', {
|
|
37
|
+
get: () => mockContext,
|
|
38
|
+
configurable: true
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
console.error = mock();
|
|
44
|
+
process.exit = mock() as any;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
console.error = originalConsoleError;
|
|
49
|
+
process.exit = originalProcessExit;
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
mock.restore();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('registerConnection', () => {
|
|
58
|
+
it('should register a new connection', () => {
|
|
59
|
+
// Mock Service.exists to return false (connection doesn't exist)
|
|
60
|
+
const existsSpy = spyOn(Service, 'exists').mockReturnValueOnce(false);
|
|
61
|
+
const instanceSpy = spyOn(Service, 'instance');
|
|
62
|
+
|
|
63
|
+
// Call the function
|
|
64
|
+
const result = Database.registerConnection('test', { connection: 'sqlite:test.db' });
|
|
65
|
+
|
|
66
|
+
// Check if Service.exists was called with the correct connection name
|
|
67
|
+
expect(existsSpy).toHaveBeenCalledWith('db.connection.test');
|
|
68
|
+
|
|
69
|
+
// Check if Service.instance was called with the correct parameters
|
|
70
|
+
expect(instanceSpy).toHaveBeenCalledWith('db.connection.test', {
|
|
71
|
+
mockDrizzleInstance: true
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Check if the result is correct
|
|
75
|
+
expect(result as any).toEqual({ mockDrizzleInstance: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should exit if the connection already exists', () => {
|
|
79
|
+
// Mock Service.exists to return true (connection exists)
|
|
80
|
+
spyOn(Service, 'exists').mockReturnValueOnce(true);
|
|
81
|
+
|
|
82
|
+
// Call the function
|
|
83
|
+
Database.registerConnection('test', { connection: 'sqlite:test.db' });
|
|
84
|
+
|
|
85
|
+
// Check if console.error and process.exit were called
|
|
86
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
87
|
+
expect.stringContaining('A connection with the name test has already been registered')
|
|
88
|
+
);
|
|
89
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('getConnection', () => {
|
|
94
|
+
it('should retrieve an existing connection', () => {
|
|
95
|
+
// Mock Service.exists to return true (connection exists)
|
|
96
|
+
const existsSpy = spyOn(Service, 'exists').mockReturnValueOnce(true);
|
|
97
|
+
|
|
98
|
+
// Mock Service.get to return a mock connection
|
|
99
|
+
const mockConnection = { mockConnection: true };
|
|
100
|
+
const getSpy = spyOn(Service, 'get').mockReturnValue(mockConnection);
|
|
101
|
+
|
|
102
|
+
// Call the function
|
|
103
|
+
const result = Database.getConnection('test');
|
|
104
|
+
|
|
105
|
+
// Check if Service.exists was called with the correct connection name
|
|
106
|
+
expect(existsSpy).toHaveBeenCalledWith('db.connection.test');
|
|
107
|
+
|
|
108
|
+
// Check if Service.get was called with the correct connection name
|
|
109
|
+
expect(getSpy).toHaveBeenCalledWith('db.connection.test');
|
|
110
|
+
|
|
111
|
+
// Check if the result is correct
|
|
112
|
+
expect(result as any).toEqual(mockConnection);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should exit if the connection does not exist', () => {
|
|
116
|
+
// Mock Service.exists to return false (connection doesn't exist)
|
|
117
|
+
spyOn(Service, 'exists').mockReturnValueOnce(false);
|
|
118
|
+
|
|
119
|
+
// Call the function
|
|
120
|
+
Database.getConnection('test');
|
|
121
|
+
|
|
122
|
+
// Check if console.error and process.exit were called
|
|
123
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
124
|
+
expect.stringContaining('No connection with name test found')
|
|
125
|
+
);
|
|
126
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('connectionExists', () => {
|
|
131
|
+
it('should return true if the connection exists', () => {
|
|
132
|
+
// Mock Service.exists to return true
|
|
133
|
+
const existsSpy = spyOn(Service, 'exists').mockReturnValueOnce(true);
|
|
134
|
+
|
|
135
|
+
// Call the function
|
|
136
|
+
const result = Database.connectionExists('test');
|
|
137
|
+
|
|
138
|
+
// Check if Service.exists was called with the correct connection name
|
|
139
|
+
expect(existsSpy).toHaveBeenCalledWith('db.connection.test');
|
|
140
|
+
|
|
141
|
+
// Check if the result is correct
|
|
142
|
+
expect(result).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return false if the connection does not exist', () => {
|
|
146
|
+
// Mock Service.exists to return false
|
|
147
|
+
const existsSpy = spyOn(Service, 'exists').mockReturnValueOnce(false);
|
|
148
|
+
|
|
149
|
+
// Call the function
|
|
150
|
+
const result = Database.connectionExists('test');
|
|
151
|
+
|
|
152
|
+
// Check if Service.exists was called with the correct connection name
|
|
153
|
+
expect(existsSpy).toHaveBeenCalledWith('db.connection.test');
|
|
154
|
+
|
|
155
|
+
// Check if the result is correct
|
|
156
|
+
expect(result).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('getDefaultConnection', () => {
|
|
161
|
+
it('should retrieve the default connection', () => {
|
|
162
|
+
// Mock Service to return the mock connection
|
|
163
|
+
const mockConnection = { mockConnection: true };
|
|
164
|
+
spyOn(Service, 'exists').mockReturnValueOnce(true);
|
|
165
|
+
spyOn(Service, 'get').mockReturnValue(mockConnection);
|
|
166
|
+
|
|
167
|
+
// Call the function
|
|
168
|
+
const result = Database.getDefaultConnection();
|
|
169
|
+
|
|
170
|
+
// Check if the result is correct
|
|
171
|
+
expect(result as any).toEqual(mockConnection);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('setDefaultConnection', () => {
|
|
176
|
+
it('should set the default connection', () => {
|
|
177
|
+
const instanceSpy = spyOn(Service, 'instance');
|
|
178
|
+
const removeSpy = spyOn(Service, 'remove');
|
|
179
|
+
spyOn(Service, 'exists').mockReturnValueOnce(true);
|
|
180
|
+
|
|
181
|
+
// Mock Service.get to return a mock connection
|
|
182
|
+
const mockConnection = { mockConnection: true };
|
|
183
|
+
spyOn(Service, 'get').mockReturnValue(mockConnection);
|
|
184
|
+
|
|
185
|
+
// Call the function
|
|
186
|
+
const result = Database.setDefaultConnection('test');
|
|
187
|
+
|
|
188
|
+
// Check if Service.remove was called with the correct connection name
|
|
189
|
+
expect(removeSpy).toHaveBeenCalledWith('db.connection.default');
|
|
190
|
+
|
|
191
|
+
// Check if Service.instance was called with the correct parameters
|
|
192
|
+
expect(instanceSpy).toHaveBeenCalledWith('db.connection.default', mockConnection);
|
|
193
|
+
|
|
194
|
+
// Check if the result is correct
|
|
195
|
+
expect(result as any).toEqual(mockConnection);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|