@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/cache.d.ts +60 -0
  3. package/dist/commands/migration_run.command.d.ts +13 -0
  4. package/dist/commands/project.d.ts +11 -0
  5. package/dist/database.d.ts +82 -31
  6. package/dist/driver.d.ts +39 -0
  7. package/dist/drivers/bun-sql.d.ts +25 -0
  8. package/dist/drivers/bun-sql.js +1 -0
  9. package/dist/drivers/hyperdrive.d.ts +46 -0
  10. package/dist/drivers/hyperdrive.js +1 -0
  11. package/dist/drivers/node-postgres.d.ts +23 -0
  12. package/dist/drivers/node-postgres.js +1 -0
  13. package/dist/drivers/postgres-js.d.ts +25 -0
  14. package/dist/drivers/postgres-js.js +1 -0
  15. package/dist/drizzle.module.d.ts +3 -3
  16. package/dist/index.d.ts +5 -3
  17. package/dist/index.js +11 -1
  18. package/dist/migrations.d.ts +12 -0
  19. package/dist/model.d.ts +9 -9
  20. package/dist/query.d.ts +28 -0
  21. package/dist/repository.d.ts +34 -6
  22. package/dist/tenancy.d.ts +25 -8
  23. package/package.json +63 -11
  24. package/.omc/state/agent-replay-3920b579-6d8e-4f5f-924a-319202936685.jsonl +0 -7
  25. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/last-tool-error-state.json +0 -7
  26. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/mission-state.json +0 -93
  27. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/pre-tool-advisory-throttle.json +0 -18
  28. package/.omc/state/sessions/3920b579-6d8e-4f5f-924a-319202936685/subagent-tracking-state.json +0 -61
  29. package/.turbo/turbo-build.log +0 -3
  30. package/dist/commands/migration_generate.command.js +0 -1
  31. package/dist/commands/migration_run.command.js +0 -15
  32. package/dist/database.js +0 -1
  33. package/dist/drizzle.module.js +0 -1
  34. package/dist/migrations.js +0 -1
  35. package/dist/model.js +0 -1
  36. package/dist/repository.js +0 -1
  37. package/dist/table.js +0 -1
  38. package/dist/tenancy.js +0 -1
  39. package/scripts/minify.ts +0 -10
  40. package/src/commands/migration_generate.command.ts +0 -101
  41. package/src/commands/migration_run.command.ts +0 -160
  42. package/src/database.ts +0 -205
  43. package/src/drizzle.module.ts +0 -97
  44. package/src/index.ts +0 -47
  45. package/src/migrations.ts +0 -100
  46. package/src/model.ts +0 -178
  47. package/src/repository.ts +0 -113
  48. package/src/table.ts +0 -70
  49. package/src/tenancy.ts +0 -249
  50. package/tests/drizzle.module.test.ts +0 -201
  51. package/tests/migrations.test.ts +0 -123
  52. package/tests/model.test.ts +0 -362
  53. package/tests/setup.ts +0 -0
  54. package/tests/table.test.ts +0 -72
  55. package/tests/tenancy.test.ts +0 -212
  56. 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
- });
@@ -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
- });