@elysiumjs/mnemosyne-drizzle 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cache.d.ts +60 -0
  3. package/dist/cache.js +1 -0
  4. package/dist/commands/migration_generate.command.js +1 -1
  5. package/dist/commands/migration_run.command.d.ts +13 -0
  6. package/dist/commands/migration_run.command.js +6 -11
  7. package/dist/commands/project.d.ts +11 -0
  8. package/dist/commands/project.js +1 -0
  9. package/dist/database.d.ts +55 -23
  10. package/dist/database.js +1 -1
  11. package/dist/drizzle.module.d.ts +2 -2
  12. package/dist/drizzle.module.js +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +1 -1
  15. package/dist/migrations.d.ts +12 -0
  16. package/dist/migrations.js +2 -1
  17. package/dist/model.d.ts +5 -5
  18. package/dist/repository.d.ts +20 -5
  19. package/dist/repository.js +1 -1
  20. package/dist/tenancy.d.ts +25 -8
  21. package/dist/tenancy.js +1 -1
  22. package/package.json +19 -9
  23. package/.omc/state/agent-replay-3920b579-6d8e-4f5f-924a-319202936685.jsonl +0 -7
  24. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/last-tool-error-state.json +0 -7
  25. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/mission-state.json +0 -93
  26. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/pre-tool-advisory-throttle.json +0 -18
  27. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/subagent-tracking-state.json +0 -61
  28. package/.turbo/turbo-build.log +0 -3
  29. package/scripts/minify.ts +0 -10
  30. package/src/commands/migration_generate.command.ts +0 -101
  31. package/src/commands/migration_run.command.ts +0 -160
  32. package/src/database.ts +0 -205
  33. package/src/drizzle.module.ts +0 -97
  34. package/src/index.ts +0 -47
  35. package/src/migrations.ts +0 -100
  36. package/src/model.ts +0 -178
  37. package/src/repository.ts +0 -113
  38. package/src/table.ts +0 -70
  39. package/src/tenancy.ts +0 -249
  40. package/tests/drizzle.module.test.ts +0 -201
  41. package/tests/migrations.test.ts +0 -123
  42. package/tests/model.test.ts +0 -362
  43. package/tests/setup.ts +0 -0
  44. package/tests/table.test.ts +0 -72
  45. package/tests/tenancy.test.ts +0 -212
  46. package/tsconfig.json +0 -12
package/src/repository.ts DELETED
@@ -1,113 +0,0 @@
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
- };
package/src/table.ts DELETED
@@ -1,70 +0,0 @@
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 { AnyPgColumnBuilder, 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, AnyPgColumnBuilder>>();
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, AnyPgColumnBuilder> | 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, AnyPgColumnBuilder>
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 DELETED
@@ -1,249 +0,0 @@
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 { AnyPgColumnBuilder, PgAsyncTransaction, PgSchema, PgTable } 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, AnyPgColumnBuilder>
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: PgAsyncTransaction<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: PgAsyncTransaction<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: PgAsyncTransaction<any, any>) => {
245
- await tx.execute(sql`SELECT set_config(${varName}, ${tenant}, true)`);
246
- return callback();
247
- });
248
- }
249
- }
@@ -1,201 +0,0 @@
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 { Application, Event, Service } from '@elysiumjs/core';
16
- import { getTenancyConfig, getTenancyStrategy } from '@elysiumjs/mnemosyne';
17
- import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
18
-
19
- import * as pkg from '../src';
20
- import { Database, DrizzleDatabaseCache, DrizzleModule, DrizzleRLSTenancy, DrizzleSchemaTenancy } from '../src';
21
-
22
- const URL = 'postgres://u:p@localhost:5432/db';
23
-
24
- const stubClient = () => ({
25
- options: {},
26
- connect: mock(async () => {}),
27
- begin: mock(async (_callback: unknown) => {}),
28
- close: mock(async () => {})
29
- });
30
-
31
- describe('DrizzleModule', () => {
32
- let app: Awaited<ReturnType<typeof Application.testing>> | undefined;
33
-
34
- afterEach(async () => {
35
- await app?.shutdown();
36
- app = undefined;
37
- });
38
-
39
- it('registers nothing when the package is imported', async () => {
40
- const once = spyOn(Event, 'once');
41
- const on = spyOn(Event, 'on');
42
- await import('../src/index.ts?fresh-import');
43
- expect(once).not.toHaveBeenCalled();
44
- expect(on).not.toHaveBeenCalled();
45
- once.mockRestore();
46
- on.mockRestore();
47
- expect(Service.exists('db.connection.default')).toBe(false);
48
- expect(Service.exists('mnemosyne.tenancy.config')).toBe(false);
49
- expect(typeof pkg.DrizzleModule.with).toBe('function');
50
- });
51
-
52
- it('provides connections, the default alias and the schema strategy by default', async () => {
53
- app = await Application.testing({
54
- modules: [DrizzleModule.with({ default: 'main', connections: { main: { connection: URL } } })],
55
- boot: 'eager'
56
- });
57
-
58
- expect(Database.connectionExists('main')).toBe(true);
59
- expect(Database.connectionExists('default')).toBe(true);
60
- const main = Database.getConnection('main');
61
- expect(main).toBeDefined();
62
- expect(Database.getConnection()).toBe(main);
63
- expect(getTenancyConfig()).toEqual({ mode: 'schema' });
64
- expect(getTenancyStrategy()).toBeInstanceOf(DrizzleSchemaTenancy);
65
- });
66
-
67
- it('uses the RLS strategy when tenancy mode is rls', async () => {
68
- app = await Application.testing({
69
- modules: [
70
- DrizzleModule.with({
71
- connections: { default: { connection: URL } },
72
- tenancy: { mode: 'rls' }
73
- })
74
- ],
75
- boot: 'eager'
76
- });
77
-
78
- expect(getTenancyConfig()).toEqual({ mode: 'rls' });
79
- expect(getTenancyStrategy()).toBeInstanceOf(DrizzleRLSTenancy);
80
- });
81
-
82
- it('rejects a default that is not a declared connection', async () => {
83
- await expect(
84
- Application.testing({
85
- modules: [DrizzleModule.with({ default: 'nope', connections: { main: { connection: URL } } })]
86
- })
87
- ).rejects.toThrow(/nope/);
88
- });
89
-
90
- it('builds a connection with a cache on a lazy boot', async () => {
91
- app = await Application.testing({
92
- modules: [
93
- DrizzleModule.with({
94
- connections: { default: { connection: URL, cache: { storage: 'memory' } } }
95
- })
96
- ],
97
- boot: 'lazy'
98
- });
99
-
100
- expect(await app.get('db.connection.default')).toBeDefined();
101
- });
102
-
103
- it('resolves its cache storage on first use', async () => {
104
- app = await Application.testing({
105
- modules: [DrizzleModule.with({ connections: { default: { connection: URL } } })],
106
- boot: 'eager'
107
- });
108
- const cache = new DrizzleDatabaseCache('all', 1, 'memory');
109
- await cache.put('k', [1], ['t'], false);
110
- expect(await cache.get('k')).toEqual([1]);
111
- });
112
-
113
- it('closes only the connections that were created', async () => {
114
- const used = stubClient();
115
- const unused = stubClient();
116
- app = await Application.testing({
117
- modules: [
118
- DrizzleModule.with({
119
- default: 'a',
120
- connections: { a: { client: used as never }, b: { client: unused as never } }
121
- })
122
- ],
123
- boot: 'lazy'
124
- });
125
- await app.get('db.connection.default');
126
-
127
- await app.shutdown();
128
- app = undefined;
129
-
130
- expect(used.close).toHaveBeenCalledTimes(1);
131
- expect(unused.close).not.toHaveBeenCalled();
132
- });
133
- });
134
-
135
- describe('migration commands', () => {
136
- const originalExit = process.exit;
137
- const originalStderr = process.stderr.write;
138
- const originalStdout = process.stdout.write;
139
- let errors = '';
140
-
141
- beforeEach(() => {
142
- errors = '';
143
- process.exit = mock() as never;
144
- process.stderr.write = mock((message: string) => {
145
- errors += message;
146
- return true;
147
- }) as never;
148
- process.stdout.write = mock(() => true) as never;
149
- });
150
-
151
- let started: Application | undefined;
152
-
153
- afterEach(async () => {
154
- await started?.kernel.shutdown();
155
- started = undefined;
156
- Service.clear();
157
- process.exit = originalExit;
158
- process.stderr.write = originalStderr;
159
- process.stdout.write = originalStdout;
160
- for (const signal of ['SIGINT', 'SIGTERM'] as const) process.removeAllListeners(signal);
161
- });
162
-
163
- const makeApp = () => {
164
- const client = stubClient();
165
- @Application.register({
166
- modules: [
167
- DrizzleModule.with({ connections: { default: { client: client as never } } })
168
- ]
169
- })
170
- class TestApp extends Application {}
171
- const app = new TestApp();
172
- started = app;
173
- return { app, client };
174
- };
175
-
176
- it('migration:run finds db.connection.default', async () => {
177
- const { app, client } = makeApp();
178
- await app.run(['migration:run', '--tenant', 'public']);
179
-
180
- expect(errors).not.toContain('does not exist');
181
- expect(client.connect).toHaveBeenCalled();
182
- expect(client.begin).toHaveBeenCalled();
183
- expect(Database.connectionExists('default')).toBe(true);
184
- });
185
-
186
- it('lists the migration commands under NODE_ENV=production', async () => {
187
- const previous = Bun.env.NODE_ENV;
188
- Bun.env.NODE_ENV = 'production';
189
- try {
190
- const { app } = makeApp();
191
- await app.run(['list']);
192
- const names = (app.kernel.graph.collect('commands') as { item: { command: string } }[]).map(
193
- ({ item }) => item.command
194
- );
195
- expect(names).toContain('migration:run');
196
- expect(names).toContain('migration:generate');
197
- } finally {
198
- Bun.env.NODE_ENV = previous;
199
- }
200
- });
201
- });