@elysiumjs/mnemosyne-drizzle 0.2.0 → 0.4.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/CHANGELOG.md +112 -0
- package/dist/cache.d.ts +60 -0
- package/dist/commands/migration_run.command.d.ts +13 -0
- package/dist/commands/project.d.ts +11 -0
- package/dist/database.d.ts +82 -31
- package/dist/driver.d.ts +39 -0
- package/dist/drivers/bun-sql.d.ts +25 -0
- package/dist/drivers/bun-sql.js +1 -0
- package/dist/drivers/hyperdrive.d.ts +46 -0
- package/dist/drivers/hyperdrive.js +1 -0
- package/dist/drivers/node-postgres.d.ts +23 -0
- package/dist/drivers/node-postgres.js +1 -0
- package/dist/drivers/postgres-js.d.ts +25 -0
- package/dist/drivers/postgres-js.js +1 -0
- package/dist/drizzle.module.d.ts +3 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +11 -1
- package/dist/migrations.d.ts +12 -0
- package/dist/model.d.ts +9 -9
- package/dist/query.d.ts +28 -0
- package/dist/repository.d.ts +34 -6
- package/dist/tenancy.d.ts +25 -8
- package/package.json +63 -11
- package/.omc/state/agent-replay-3920b579-6d8e-4f5f-924a-319202936685.jsonl +0 -7
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/last-tool-error-state.json +0 -7
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/mission-state.json +0 -93
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/pre-tool-advisory-throttle.json +0 -18
- package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/subagent-tracking-state.json +0 -61
- package/.turbo/turbo-build.log +0 -3
- package/dist/commands/migration_generate.command.js +0 -1
- package/dist/commands/migration_run.command.js +0 -15
- package/dist/database.js +0 -1
- package/dist/drizzle.module.js +0 -1
- package/dist/migrations.js +0 -1
- package/dist/model.js +0 -1
- package/dist/repository.js +0 -1
- package/dist/table.js +0 -1
- package/dist/tenancy.js +0 -1
- package/scripts/minify.ts +0 -10
- package/src/commands/migration_generate.command.ts +0 -101
- package/src/commands/migration_run.command.ts +0 -160
- package/src/database.ts +0 -205
- package/src/drizzle.module.ts +0 -97
- package/src/index.ts +0 -47
- package/src/migrations.ts +0 -100
- package/src/model.ts +0 -178
- package/src/repository.ts +0 -113
- package/src/table.ts +0 -70
- package/src/tenancy.ts +0 -249
- package/tests/drizzle.module.test.ts +0 -201
- package/tests/migrations.test.ts +0 -123
- package/tests/model.test.ts +0 -362
- package/tests/setup.ts +0 -0
- package/tests/table.test.ts +0 -72
- package/tests/tenancy.test.ts +0 -212
- package/tsconfig.json +0 -12
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
|
-
});
|
package/tests/migrations.test.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
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 { describe, expect, it } from 'bun:test';
|
|
16
|
-
|
|
17
|
-
import { getMigrationName, planMigrations } from '../src/migrations';
|
|
18
|
-
|
|
19
|
-
const dir = '/app/src/database/migrations';
|
|
20
|
-
|
|
21
|
-
// Records applied after every migration in these tests was generated.
|
|
22
|
-
const applied = (...names: string[]) =>
|
|
23
|
-
names.map((name) => ({ name, appliedAt: new Date('2027-01-01T00:00:00Z') }));
|
|
24
|
-
|
|
25
|
-
describe('getMigrationName', () => {
|
|
26
|
-
it('should use the file name of a flat migration', () => {
|
|
27
|
-
expect(getMigrationName(`${dir}/0000_users.sql`)).toBe('0000_users.sql');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should use the folder name of a Drizzle Kit 1.0 migration', () => {
|
|
31
|
-
expect(getMigrationName(`${dir}/20260101120000_users/migration.sql`)).toBe(
|
|
32
|
-
'20260101120000_users'
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('planMigrations', () => {
|
|
38
|
-
it('should run every migration on an empty database, in order', () => {
|
|
39
|
-
const files = [
|
|
40
|
-
`${dir}/20260102000000_posts/migration.sql`,
|
|
41
|
-
`${dir}/20260101000000_users/migration.sql`
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
expect(planMigrations(files, applied())).toEqual({
|
|
45
|
-
pending: [
|
|
46
|
-
`${dir}/20260101000000_users/migration.sql`,
|
|
47
|
-
`${dir}/20260102000000_posts/migration.sql`
|
|
48
|
-
],
|
|
49
|
-
renames: []
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should skip migrations already applied under their current name', () => {
|
|
54
|
-
const files = [`${dir}/0000_users.sql`, `${dir}/20260101000000_posts/migration.sql`];
|
|
55
|
-
|
|
56
|
-
expect(planMigrations(files, applied('0000_users.sql', '20260101000000_posts'))).toEqual({
|
|
57
|
-
pending: [],
|
|
58
|
-
renames: []
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should recognise flat migrations that drizzle-kit up moved into folders', () => {
|
|
63
|
-
const files = [
|
|
64
|
-
`${dir}/20250301101500_users/migration.sql`,
|
|
65
|
-
`${dir}/20250302090000_posts/migration.sql`,
|
|
66
|
-
`${dir}/20260101000000_comments/migration.sql`
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
expect(planMigrations(files, applied('0000_users.sql', '0001_posts.sql'))).toEqual({
|
|
70
|
-
pending: [`${dir}/20260101000000_comments/migration.sql`],
|
|
71
|
-
renames: [
|
|
72
|
-
{ from: '0000_users.sql', to: '20250301101500_users' },
|
|
73
|
-
{ from: '0001_posts.sql', to: '20250302090000_posts' }
|
|
74
|
-
]
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should match timestamp-prefixed flat migrations whose prefix differs from the folder', () => {
|
|
79
|
-
const files = [`${dir}/20250301101501_add_users_table/migration.sql`];
|
|
80
|
-
|
|
81
|
-
expect(planMigrations(files, applied('20250301101459_add_users_table.sql'))).toEqual({
|
|
82
|
-
pending: [],
|
|
83
|
-
renames: [
|
|
84
|
-
{ from: '20250301101459_add_users_table.sql', to: '20250301101501_add_users_table' }
|
|
85
|
-
]
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should pair repeated names in order', () => {
|
|
90
|
-
const files = [
|
|
91
|
-
`${dir}/20250301000000_update/migration.sql`,
|
|
92
|
-
`${dir}/20250302000000_update/migration.sql`,
|
|
93
|
-
`${dir}/20250303000000_update/migration.sql`
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
expect(planMigrations(files, applied('0000_update.sql', '0001_update.sql'))).toEqual({
|
|
97
|
-
pending: [`${dir}/20250303000000_update/migration.sql`],
|
|
98
|
-
renames: [
|
|
99
|
-
{ from: '0000_update.sql', to: '20250301000000_update' },
|
|
100
|
-
{ from: '0001_update.sql', to: '20250302000000_update' }
|
|
101
|
-
]
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should not move a record whose flat file is still there', () => {
|
|
106
|
-
const files = [`${dir}/0000_users.sql`, `${dir}/20260101000000_users/migration.sql`];
|
|
107
|
-
|
|
108
|
-
expect(planMigrations(files, applied('0000_users.sql'))).toEqual({
|
|
109
|
-
pending: [`${dir}/20260101000000_users/migration.sql`],
|
|
110
|
-
renames: []
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should run a new migration that reuses the name of a deleted one', () => {
|
|
115
|
-
const files = [`${dir}/20260101000000_update/migration.sql`];
|
|
116
|
-
const records = [{ name: '0003_update.sql', appliedAt: new Date('2025-06-01T00:00:00Z') }];
|
|
117
|
-
|
|
118
|
-
expect(planMigrations(files, records)).toEqual({
|
|
119
|
-
pending: [`${dir}/20260101000000_update/migration.sql`],
|
|
120
|
-
renames: []
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
});
|