@elysiumjs/mnemosyne-drizzle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +9 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/coverage/.lcov.info.01ffde0d9f956dce.tmp +14 -0
- package/coverage/.lcov.info.2f80a4def3a82099.tmp +343 -0
- package/coverage/.lcov.info.560a2daaba0934cd.tmp +366 -0
- package/coverage/.lcov.info.7162c9b788dc0bc6.tmp +366 -0
- package/coverage/.lcov.info.b57c0b452c3901e5.tmp +17 -0
- package/coverage/.lcov.info.c47de8626f331696.tmp +17 -0
- package/coverage/.lcov.info.c710a42b25b08349.tmp +343 -0
- package/coverage/.lcov.info.dbc8f065467fc8a2.tmp +287 -0
- package/coverage/.lcov.info.de180a43301f82c0.tmp +35 -0
- package/coverage/.lcov.info.df18163ab929ab1b.tmp +357 -0
- package/coverage/.lcov.info.febf711c192f5c0c.tmp +17 -0
- package/coverage/lcov.info +357 -0
- package/dist/database.d.ts +71 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/model.d.ts +61 -0
- package/dist/repository.d.ts +35 -0
- package/dist/table.d.ts +145 -0
- package/dist/tenancy.d.ts +88 -0
- package/package.json +38 -0
- package/src/database.ts +175 -0
- package/src/index.ts +69 -0
- package/src/model.ts +171 -0
- package/src/repository.ts +113 -0
- package/src/table.ts +70 -0
- package/src/tenancy.ts +249 -0
- package/tests/database.test.ts +198 -0
- package/tests/model.test.ts +362 -0
- package/tests/setup.ts +10 -0
- package/tests/table.test.ts +72 -0
- package/tests/tenancy.test.ts +212 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,362 @@
|
|
|
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, describe, expect, it, jest, mock, spyOn } from 'bun:test';
|
|
16
|
+
import * as d from 'drizzle-orm/pg-core';
|
|
17
|
+
import { t } from 'elysia';
|
|
18
|
+
|
|
19
|
+
import { Application } from '@elysiumjs/core';
|
|
20
|
+
|
|
21
|
+
import { createSchemaFromDrizzle, drizzleAdapter, DrizzleModel } from '../src/model';
|
|
22
|
+
import { pgTable } from '../src/table';
|
|
23
|
+
import * as Tenancy from '../src/tenancy';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Test Fixtures
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
const mockColumns = {
|
|
30
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
31
|
+
name: d.varchar().notNull(),
|
|
32
|
+
age: d.integer().notNull(),
|
|
33
|
+
email: d.varchar().notNull().unique(),
|
|
34
|
+
is_confirmed: d.boolean().default(false)
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mockTable = d.pgTable('users', mockColumns);
|
|
38
|
+
|
|
39
|
+
const enhancedMockTable = pgTable('enhanced_users', mockColumns);
|
|
40
|
+
|
|
41
|
+
const mockStore = new Map([['tenant', 'test-tenant']]);
|
|
42
|
+
const mockGetStore = mock(() => mockStore);
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Mock Application context
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
Object.defineProperty(Application, 'context', {
|
|
50
|
+
get: () => ({
|
|
51
|
+
getStore: mockGetStore,
|
|
52
|
+
run: mock((_: any, callback: any) => callback())
|
|
53
|
+
}),
|
|
54
|
+
configurable: true
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Tests
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
describe('DrizzleModel', () => {
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
jest.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
mock.restore();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('drizzleAdapter', () => {
|
|
72
|
+
describe('getTableName', () => {
|
|
73
|
+
it('should return the table name', () => {
|
|
74
|
+
const name = drizzleAdapter.getTableName(mockTable);
|
|
75
|
+
expect(name).toBe('users');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('getColumns', () => {
|
|
80
|
+
it('should return normalized column metadata', () => {
|
|
81
|
+
const columns = drizzleAdapter.getColumns(mockTable);
|
|
82
|
+
|
|
83
|
+
expect(columns).toBeArrayOfSize(5);
|
|
84
|
+
|
|
85
|
+
const idCol = columns.find(c => c.name === 'id');
|
|
86
|
+
expect(idCol).toBeDefined();
|
|
87
|
+
expect(idCol!.dataType).toBe('uuid');
|
|
88
|
+
expect(idCol!.isPrimaryKey).toBe(true);
|
|
89
|
+
expect(idCol!.hasDefault).toBe(true);
|
|
90
|
+
expect(idCol!.nullable).toBe(false);
|
|
91
|
+
|
|
92
|
+
const nameCol = columns.find(c => c.name === 'name');
|
|
93
|
+
expect(nameCol).toBeDefined();
|
|
94
|
+
expect(nameCol!.dataType).toBe('string');
|
|
95
|
+
expect(nameCol!.nullable).toBe(false);
|
|
96
|
+
expect(nameCol!.hasDefault).toBe(false);
|
|
97
|
+
|
|
98
|
+
const ageCol = columns.find(c => c.name === 'age');
|
|
99
|
+
expect(ageCol).toBeDefined();
|
|
100
|
+
expect(ageCol!.dataType).toBe('number');
|
|
101
|
+
|
|
102
|
+
const confirmedCol = columns.find(c => c.name === 'is_confirmed');
|
|
103
|
+
expect(confirmedCol).toBeDefined();
|
|
104
|
+
expect(confirmedCol!.dataType).toBe('boolean');
|
|
105
|
+
expect(confirmedCol!.hasDefault).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('createTenantTable', () => {
|
|
110
|
+
it('should create a tenant-scoped table when column builders are registered', () => {
|
|
111
|
+
// Use enhanced pgTable which auto-registers builders
|
|
112
|
+
const cols = {
|
|
113
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
114
|
+
name: d.varchar().notNull()
|
|
115
|
+
};
|
|
116
|
+
const table = pgTable('items', cols);
|
|
117
|
+
|
|
118
|
+
const pgSchemaSpy = spyOn(d, 'pgSchema');
|
|
119
|
+
|
|
120
|
+
const tenantTable = drizzleAdapter.createTenantTable(table, 'tenant-abc');
|
|
121
|
+
|
|
122
|
+
expect(pgSchemaSpy).toHaveBeenCalledWith('tenant-abc');
|
|
123
|
+
expect(tenantTable).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should create a public table for the public tenant', () => {
|
|
127
|
+
const cols = {
|
|
128
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
129
|
+
name: d.varchar().notNull()
|
|
130
|
+
};
|
|
131
|
+
const table = pgTable('items_public', cols);
|
|
132
|
+
|
|
133
|
+
const pgTableSpy = spyOn(d, 'pgTable');
|
|
134
|
+
|
|
135
|
+
const publicTable = drizzleAdapter.createTenantTable(table, 'public');
|
|
136
|
+
|
|
137
|
+
expect(pgTableSpy).toHaveBeenCalledWith('items_public', cols);
|
|
138
|
+
expect(publicTable).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should throw if column builders are not registered', () => {
|
|
142
|
+
const unregisteredTable = d.pgTable('orphan', {
|
|
143
|
+
id: d.uuid().primaryKey()
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(() => {
|
|
147
|
+
drizzleAdapter.createTenantTable(unregisteredTable, 'some-tenant');
|
|
148
|
+
}).toThrow('No column builders registered');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('createSchemaFromDrizzle', () => {
|
|
154
|
+
it('should create a schema for select mode', () => {
|
|
155
|
+
const getTableConfigSpy = spyOn(d, 'getTableConfig');
|
|
156
|
+
const objectSpy = spyOn(t, 'Object');
|
|
157
|
+
|
|
158
|
+
createSchemaFromDrizzle(mockTable, { mode: 'select' });
|
|
159
|
+
|
|
160
|
+
expect(getTableConfigSpy).toHaveBeenCalledWith(mockTable);
|
|
161
|
+
expect(objectSpy).toHaveBeenCalledWith(
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
id: expect.any(Object),
|
|
164
|
+
name: expect.any(Object),
|
|
165
|
+
email: expect.any(Object),
|
|
166
|
+
age: expect.any(Object),
|
|
167
|
+
is_confirmed: expect.any(Object)
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
objectSpy.mockRestore();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should create a schema for create mode', () => {
|
|
175
|
+
const getTableConfigSpy = spyOn(d, 'getTableConfig');
|
|
176
|
+
const objectSpy = spyOn(t, 'Object');
|
|
177
|
+
|
|
178
|
+
createSchemaFromDrizzle(mockTable, { mode: 'create' });
|
|
179
|
+
|
|
180
|
+
expect(getTableConfigSpy).toHaveBeenCalledWith(mockTable);
|
|
181
|
+
|
|
182
|
+
// Primary key should be excluded in create mode
|
|
183
|
+
expect(objectSpy).toHaveBeenCalledWith(
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
name: expect.any(Object),
|
|
186
|
+
email: expect.any(Object),
|
|
187
|
+
age: expect.any(Object),
|
|
188
|
+
is_confirmed: expect.any(Object)
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(objectSpy).not.toHaveBeenCalledWith(
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
id: expect.any(Object)
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
objectSpy.mockRestore();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should create a schema for update mode', () => {
|
|
202
|
+
const getTableConfigSpy = spyOn(d, 'getTableConfig');
|
|
203
|
+
const objectSpy = spyOn(t, 'Object');
|
|
204
|
+
|
|
205
|
+
createSchemaFromDrizzle(mockTable, { mode: 'update' });
|
|
206
|
+
|
|
207
|
+
expect(getTableConfigSpy).toHaveBeenCalledWith(mockTable);
|
|
208
|
+
|
|
209
|
+
// Primary key should be excluded in update mode
|
|
210
|
+
expect(objectSpy).toHaveBeenCalledWith(
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
name: expect.any(Object),
|
|
213
|
+
email: expect.any(Object),
|
|
214
|
+
age: expect.any(Object),
|
|
215
|
+
is_confirmed: expect.any(Object)
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(objectSpy).not.toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({
|
|
221
|
+
id: expect.any(Object)
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
objectSpy.mockRestore();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('DrizzleModel with pgTable', () => {
|
|
230
|
+
describe('single signature API', () => {
|
|
231
|
+
it('should accept a pgTable directly', () => {
|
|
232
|
+
const cols = {
|
|
233
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
234
|
+
title: d.varchar().notNull()
|
|
235
|
+
};
|
|
236
|
+
const posts = pgTable('posts', cols);
|
|
237
|
+
|
|
238
|
+
class Post extends DrizzleModel(posts) {}
|
|
239
|
+
|
|
240
|
+
expect(Post.tableName).toBe('posts');
|
|
241
|
+
expect(Post.table).toBeDefined();
|
|
242
|
+
expect(Post.insertSchema).toBeDefined();
|
|
243
|
+
expect(Post.updateSchema).toBeDefined();
|
|
244
|
+
expect(Post.selectSchema).toBeDefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should support supportTenancy option', () => {
|
|
248
|
+
const cols = {
|
|
249
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
250
|
+
name: d.varchar().notNull()
|
|
251
|
+
};
|
|
252
|
+
const tenants = pgTable('tenants', cols);
|
|
253
|
+
|
|
254
|
+
class Tenant extends DrizzleModel(tenants, { supportTenancy: true }) {}
|
|
255
|
+
|
|
256
|
+
expect(Tenant.supportTenancy).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should default supportTenancy to false', () => {
|
|
260
|
+
const cols = {
|
|
261
|
+
id: d.uuid().primaryKey().defaultRandom()
|
|
262
|
+
};
|
|
263
|
+
const items = pgTable('items', cols);
|
|
264
|
+
|
|
265
|
+
class Item extends DrizzleModel(items) {}
|
|
266
|
+
|
|
267
|
+
expect(Item.supportTenancy).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should infer types from the table', () => {
|
|
271
|
+
const cols = {
|
|
272
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
273
|
+
email: d.varchar().notNull(),
|
|
274
|
+
age: d.integer()
|
|
275
|
+
};
|
|
276
|
+
const users = pgTable('typed_users', cols);
|
|
277
|
+
|
|
278
|
+
class TypedUser extends DrizzleModel(users) {}
|
|
279
|
+
|
|
280
|
+
// Type inference markers
|
|
281
|
+
expect(TypedUser.$inferSelect).toBeUndefined();
|
|
282
|
+
expect(TypedUser.$inferInsert).toBeUndefined();
|
|
283
|
+
expect(TypedUser.$inferUpdate).toBeUndefined();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('tenancy with pgTable', () => {
|
|
288
|
+
it('should create tenant table when tenancy is enabled', () => {
|
|
289
|
+
const cols = {
|
|
290
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
291
|
+
data: d.varchar().notNull()
|
|
292
|
+
};
|
|
293
|
+
const tenantData = pgTable('tenant_data', cols);
|
|
294
|
+
|
|
295
|
+
class TenantData extends DrizzleModel(tenantData, { supportTenancy: true }) {}
|
|
296
|
+
|
|
297
|
+
// Should not throw when accessing table
|
|
298
|
+
expect(TenantData.table).toBeDefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('Tenancy', () => {
|
|
304
|
+
it('should register a new tenant schema', () => {
|
|
305
|
+
const pgSchemaSpy = spyOn(d, 'pgSchema');
|
|
306
|
+
|
|
307
|
+
const schema = Tenancy.registerTenantSchema('new-tenant');
|
|
308
|
+
|
|
309
|
+
expect(pgSchemaSpy).toHaveBeenCalledWith('new-tenant');
|
|
310
|
+
expect(schema).toBeDefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should wrap columns with a tenant schema', () => {
|
|
314
|
+
const pgTableSpy = spyOn(d, 'pgTable');
|
|
315
|
+
const pgSchemaSpy = spyOn(d, 'pgSchema');
|
|
316
|
+
|
|
317
|
+
const cols = {
|
|
318
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
319
|
+
name: d.varchar().notNull()
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Call the function with a non-public tenant
|
|
323
|
+
const table = Tenancy.wrapTenantSchema('wrap-test-tenant', 'items', cols);
|
|
324
|
+
|
|
325
|
+
expect(pgSchemaSpy).toHaveBeenCalledWith('wrap-test-tenant');
|
|
326
|
+
expect(table).toBeDefined();
|
|
327
|
+
|
|
328
|
+
// Call the function with the public tenant
|
|
329
|
+
const publicTable = Tenancy.wrapTenantSchema('public', 'items', cols);
|
|
330
|
+
|
|
331
|
+
expect(pgTableSpy).toHaveBeenCalledWith('items', cols);
|
|
332
|
+
expect(publicTable).toBeDefined();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should reuse existing tenant schemas', () => {
|
|
336
|
+
const pgSchemaSpy = spyOn(d, 'pgSchema');
|
|
337
|
+
|
|
338
|
+
Tenancy.registerTenantSchema('existing-tenant');
|
|
339
|
+
pgSchemaSpy.mockClear();
|
|
340
|
+
|
|
341
|
+
const schema = Tenancy.registerTenantSchema('existing-tenant');
|
|
342
|
+
|
|
343
|
+
expect(pgSchemaSpy).not.toHaveBeenCalled();
|
|
344
|
+
expect(schema).toBeDefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should reuse existing tenant tables', () => {
|
|
348
|
+
const cols = {
|
|
349
|
+
id: d.uuid().primaryKey().defaultRandom()
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const pgSchemaSpy = spyOn(d, 'pgSchema');
|
|
353
|
+
|
|
354
|
+
Tenancy.wrapTenantSchema('cache-tenant', 'cached_items', cols);
|
|
355
|
+
pgSchemaSpy.mockClear();
|
|
356
|
+
|
|
357
|
+
Tenancy.wrapTenantSchema('cache-tenant', 'cached_items', cols);
|
|
358
|
+
|
|
359
|
+
expect(pgSchemaSpy).not.toHaveBeenCalled();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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 { describe, expect, it } from 'bun:test';
|
|
16
|
+
import * as d from 'drizzle-orm/pg-core';
|
|
17
|
+
|
|
18
|
+
import { getTableBuilders, pgTable } from '../src/table';
|
|
19
|
+
|
|
20
|
+
describe('pgTable', () => {
|
|
21
|
+
describe('table creation', () => {
|
|
22
|
+
it('should create a table with the same signature as drizzle pgTable', () => {
|
|
23
|
+
const cols = {
|
|
24
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
25
|
+
name: d.varchar().notNull()
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const table = pgTable('users', cols);
|
|
29
|
+
|
|
30
|
+
expect(table).toBeDefined();
|
|
31
|
+
expect(table).toHaveProperty('id');
|
|
32
|
+
expect(table).toHaveProperty('name');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should support extraConfig parameter', () => {
|
|
36
|
+
const cols = {
|
|
37
|
+
id: d.uuid().primaryKey().defaultRandom()
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const table = pgTable('items', cols, (t) => ({
|
|
41
|
+
unique: t.unique().on(t.id)
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
expect(table).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('column builder registry', () => {
|
|
49
|
+
it('should register column builders for created tables', () => {
|
|
50
|
+
const cols = {
|
|
51
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
52
|
+
email: d.varchar().notNull()
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const table = pgTable('registered', cols);
|
|
56
|
+
const builders = getTableBuilders(table);
|
|
57
|
+
|
|
58
|
+
expect(builders).toBeDefined();
|
|
59
|
+
expect(builders).toHaveProperty('id');
|
|
60
|
+
expect(builders).toHaveProperty('email');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return undefined for tables not created with enhanced pgTable', () => {
|
|
64
|
+
const rawTable = d.pgTable('raw', {
|
|
65
|
+
id: d.uuid().primaryKey()
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const builders = getTableBuilders(rawTable);
|
|
69
|
+
expect(builders).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
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, describe, expect, it, jest, mock, spyOn } from 'bun:test';
|
|
16
|
+
import * as d from 'drizzle-orm/pg-core';
|
|
17
|
+
|
|
18
|
+
import { Application, Service } from '@elysiumjs/core';
|
|
19
|
+
|
|
20
|
+
import { Database } from '../src/database';
|
|
21
|
+
import { DrizzleModel } from '../src/model';
|
|
22
|
+
import { pgTable } from '../src/table';
|
|
23
|
+
import {
|
|
24
|
+
createRLSPolicy,
|
|
25
|
+
DrizzleRLSTenancy,
|
|
26
|
+
DrizzleSchemaTenancy,
|
|
27
|
+
getTenantSchema,
|
|
28
|
+
registerTenantSchema,
|
|
29
|
+
wrapTenantSchema
|
|
30
|
+
} from '../src/tenancy';
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Mock setup
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
mock.module('drizzle-orm/bun-sql', () => ({
|
|
37
|
+
drizzle: mock(() => ({ mockDrizzleInstance: true }))
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
const mockContext = {
|
|
42
|
+
getStore: mock(() => new Map()),
|
|
43
|
+
run: mock((_: any, callback: any) => callback())
|
|
44
|
+
};
|
|
45
|
+
Object.defineProperty(Application, 'context', {
|
|
46
|
+
get: () => mockContext,
|
|
47
|
+
configurable: true
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Tests
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
describe('Tenancy', () => {
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterAll(() => {
|
|
61
|
+
mock.restore();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('getTenantSchema', () => {
|
|
65
|
+
it('should return a PgSchema for the tenant', () => {
|
|
66
|
+
const schema = getTenantSchema('schema-test-tenant');
|
|
67
|
+
expect(schema).toBeDefined();
|
|
68
|
+
expect(schema.schemaName).toBe('schema-test-tenant');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return the same schema for the same tenant', () => {
|
|
72
|
+
const schema1 = getTenantSchema('reuse-schema-tenant');
|
|
73
|
+
const schema2 = getTenantSchema('reuse-schema-tenant');
|
|
74
|
+
expect(schema1).toBe(schema2);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('registerTenantSchema', () => {
|
|
79
|
+
it('should register and return a new tenant schema', () => {
|
|
80
|
+
const pgSchemaSpy = spyOn(d, 'pgSchema');
|
|
81
|
+
|
|
82
|
+
const schema = registerTenantSchema('register-tenant');
|
|
83
|
+
|
|
84
|
+
expect(pgSchemaSpy).toHaveBeenCalledWith('register-tenant');
|
|
85
|
+
expect(schema).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not re-register existing tenant schemas', () => {
|
|
89
|
+
const pgSchemaSpy = spyOn(d, 'pgSchema');
|
|
90
|
+
|
|
91
|
+
registerTenantSchema('already-registered');
|
|
92
|
+
pgSchemaSpy.mockClear();
|
|
93
|
+
|
|
94
|
+
const schema = registerTenantSchema('already-registered');
|
|
95
|
+
|
|
96
|
+
expect(pgSchemaSpy).not.toHaveBeenCalled();
|
|
97
|
+
expect(schema).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('wrapTenantSchema', () => {
|
|
102
|
+
it('should return a regular pgTable for the public tenant', () => {
|
|
103
|
+
const pgTableSpy = spyOn(d, 'pgTable');
|
|
104
|
+
const cols = { id: d.uuid().primaryKey().defaultRandom() };
|
|
105
|
+
|
|
106
|
+
const table = wrapTenantSchema('public', 'wrap_public', cols);
|
|
107
|
+
|
|
108
|
+
expect(pgTableSpy).toHaveBeenCalledWith('wrap_public', cols);
|
|
109
|
+
expect(table).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return a schema-qualified table for non-public tenants', () => {
|
|
113
|
+
const cols = { id: d.uuid().primaryKey().defaultRandom() };
|
|
114
|
+
|
|
115
|
+
const table = wrapTenantSchema('tenant-wrap', 'wrap_test', cols);
|
|
116
|
+
|
|
117
|
+
expect(table).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should cache tenant tables', () => {
|
|
121
|
+
const cols = { id: d.uuid().primaryKey().defaultRandom() };
|
|
122
|
+
|
|
123
|
+
const table1 = wrapTenantSchema('cached-wrap', 'cache_wrap_test', cols);
|
|
124
|
+
const table2 = wrapTenantSchema('cached-wrap', 'cache_wrap_test', cols);
|
|
125
|
+
|
|
126
|
+
expect(table1).toBe(table2);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('DrizzleSchemaTenancy', () => {
|
|
131
|
+
it('should have mode set to schema', () => {
|
|
132
|
+
const strategy = new DrizzleSchemaTenancy();
|
|
133
|
+
expect(strategy.mode).toBe('schema');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should resolve a table to a tenant-scoped table', () => {
|
|
137
|
+
const cols = {
|
|
138
|
+
id: d.uuid().primaryKey().defaultRandom(),
|
|
139
|
+
name: d.varchar().notNull()
|
|
140
|
+
};
|
|
141
|
+
const table = pgTable('schema_strategy_test', cols);
|
|
142
|
+
|
|
143
|
+
const strategy = new DrizzleSchemaTenancy();
|
|
144
|
+
const resolved = strategy.resolveTable(table, 'strategy-tenant');
|
|
145
|
+
|
|
146
|
+
expect(resolved).toBeDefined();
|
|
147
|
+
expect(resolved).not.toBe(table);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should pass through callback in withIsolation', async () => {
|
|
151
|
+
const strategy = new DrizzleSchemaTenancy();
|
|
152
|
+
const result = await strategy.withIsolation(null, 'tenant', async () => 'test-result');
|
|
153
|
+
expect(result).toBe('test-result');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('DrizzleRLSTenancy', () => {
|
|
158
|
+
it('should have mode set to rls', () => {
|
|
159
|
+
const strategy = new DrizzleRLSTenancy();
|
|
160
|
+
expect(strategy.mode).toBe('rls');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return the same table in resolveTable', () => {
|
|
164
|
+
const table = d.pgTable('rls_test', {
|
|
165
|
+
id: d.uuid().primaryKey().defaultRandom()
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const strategy = new DrizzleRLSTenancy();
|
|
169
|
+
const resolved = strategy.resolveTable(table, 'any-tenant');
|
|
170
|
+
|
|
171
|
+
expect(resolved).toBe(table);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should wrap callback in a transaction with set_config', async () => {
|
|
175
|
+
const mockTx = {
|
|
176
|
+
execute: mock(() => Promise.resolve())
|
|
177
|
+
};
|
|
178
|
+
const mockConnection = {
|
|
179
|
+
transaction: mock(async (fn: any) => fn(mockTx))
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const strategy = new DrizzleRLSTenancy();
|
|
183
|
+
const result = await strategy.withIsolation(
|
|
184
|
+
mockConnection,
|
|
185
|
+
'rls-tenant',
|
|
186
|
+
async () => 'rls-result'
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(mockConnection.transaction).toHaveBeenCalled();
|
|
190
|
+
expect(mockTx.execute).toHaveBeenCalled();
|
|
191
|
+
expect(result).toBe('rls-result');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('createRLSPolicy', () => {
|
|
196
|
+
it('should create an RLS policy with default settings', () => {
|
|
197
|
+
const policy = createRLSPolicy('users');
|
|
198
|
+
|
|
199
|
+
expect(policy).toBeDefined();
|
|
200
|
+
expect(policy.name).toBe('users_tenant_isolation');
|
|
201
|
+
expect(policy.for).toBe('all');
|
|
202
|
+
expect(policy.to).toBe('public');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should create an RLS policy with custom column and policy name', () => {
|
|
206
|
+
const policy = createRLSPolicy('orders', 'org_id', 'orders_org_policy');
|
|
207
|
+
|
|
208
|
+
expect(policy).toBeDefined();
|
|
209
|
+
expect(policy.name).toBe('orders_org_policy');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
package/tsconfig.json
ADDED