core-services-sdk 1.3.84 → 1.3.85
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/package.json +1 -1
- package/src/postgresql/pagination/paginate.js +2 -1
- package/src/postgresql/repositories/BaseRepository.js +7 -4
- package/tests/repositories/BaseRepository.int.test.js +172 -0
- package/tests/repositories/BaseRepository.test.js +73 -22
- package/tests/repositories/TenantScopedRepository.int.test.js +173 -0
- package/types/postgresql/pagination/paginate.d.ts +1 -0
- package/types/postgresql/repositories/BaseRepository.d.ts +7 -4
package/package.json
CHANGED
|
@@ -73,6 +73,7 @@ export async function sqlPaginate({
|
|
|
73
73
|
page = 1,
|
|
74
74
|
baseQuery,
|
|
75
75
|
limit = 10,
|
|
76
|
+
columns = '*',
|
|
76
77
|
filter = {},
|
|
77
78
|
snakeCase = true,
|
|
78
79
|
distinctOn,
|
|
@@ -89,7 +90,7 @@ export async function sqlPaginate({
|
|
|
89
90
|
applyPagination({ query: listQuery, page, limit })
|
|
90
91
|
|
|
91
92
|
const [rows, countResult] = await Promise.all([
|
|
92
|
-
listQuery.select(
|
|
93
|
+
listQuery.select(columns),
|
|
93
94
|
distinctOn
|
|
94
95
|
? countQuery.countDistinct(`${distinctOn} as count`).first()
|
|
95
96
|
: countQuery.count('* as count').first(),
|
|
@@ -14,13 +14,14 @@ import { toSnakeCase } from '../../core/case-mapper.js'
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export class BaseRepository {
|
|
17
|
-
constructor({ db, log, baseQueryBuilder } = {}) {
|
|
17
|
+
constructor({ db, log, columns, baseQueryBuilder } = {}) {
|
|
18
18
|
if (!db) {
|
|
19
19
|
throw new Error('BaseRepository requires db instance')
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
this.db = db
|
|
23
23
|
this.log = log
|
|
24
|
+
this._columns = columns ?? '*'
|
|
24
25
|
this._baseQueryBuilder = baseQueryBuilder
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -48,7 +49,7 @@ export class BaseRepository {
|
|
|
48
49
|
|
|
49
50
|
if (this._baseQueryBuilder) {
|
|
50
51
|
// Pass full params for optional dynamic shaping
|
|
51
|
-
|
|
52
|
+
this._baseQueryBuilder(qb, params)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
return qb
|
|
@@ -59,11 +60,12 @@ export class BaseRepository {
|
|
|
59
60
|
*/
|
|
60
61
|
async find({
|
|
61
62
|
page = 1,
|
|
63
|
+
columns,
|
|
62
64
|
limit = 10,
|
|
63
65
|
filter = {},
|
|
64
|
-
orderBy = { column: 'created_at', direction: 'desc' },
|
|
65
66
|
options = {},
|
|
66
67
|
mapRow = (row) => row,
|
|
68
|
+
orderBy = { column: 'created_at', direction: 'desc' },
|
|
67
69
|
...restParams
|
|
68
70
|
}) {
|
|
69
71
|
try {
|
|
@@ -79,10 +81,11 @@ export class BaseRepository {
|
|
|
79
81
|
return await sqlPaginate({
|
|
80
82
|
page,
|
|
81
83
|
limit,
|
|
84
|
+
mapRow,
|
|
82
85
|
orderBy,
|
|
83
86
|
baseQuery: qb,
|
|
84
|
-
mapRow,
|
|
85
87
|
filter: toSnakeCase(filter),
|
|
88
|
+
columns: columns ?? this._columns,
|
|
86
89
|
})
|
|
87
90
|
} catch (error) {
|
|
88
91
|
if (this.log) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
|
|
3
|
+
import knex from 'knex'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
stopPostgres,
|
|
7
|
+
startPostgres,
|
|
8
|
+
buildPostgresUri,
|
|
9
|
+
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
|
+
|
|
11
|
+
import { BaseRepository } from '../../src/postgresql/repositories/BaseRepository.js'
|
|
12
|
+
|
|
13
|
+
const PG_OPTIONS = {
|
|
14
|
+
port: 5445,
|
|
15
|
+
db: 'testdb',
|
|
16
|
+
user: 'testuser',
|
|
17
|
+
pass: 'testpass',
|
|
18
|
+
containerName: 'postgres-base-repo-test-5445',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
|
|
22
|
+
|
|
23
|
+
let db
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
startPostgres(PG_OPTIONS)
|
|
27
|
+
|
|
28
|
+
db = knex({
|
|
29
|
+
client: 'pg',
|
|
30
|
+
connection: DATABASE_URI,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await db.schema.createTable('customers', (table) => {
|
|
34
|
+
table.uuid('id').primary()
|
|
35
|
+
table.string('name').notNullable()
|
|
36
|
+
table.timestamp('created_at').notNullable()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
await db.schema.createTable('orders', (table) => {
|
|
40
|
+
table.uuid('id').primary()
|
|
41
|
+
table.uuid('customer_id').notNullable()
|
|
42
|
+
table.decimal('amount', 10, 2)
|
|
43
|
+
table.timestamp('created_at').notNullable()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
if (db) {
|
|
49
|
+
await db.destroy()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
stopPostgres(PG_OPTIONS.containerName)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
await db('orders').truncate()
|
|
57
|
+
await db('customers').truncate()
|
|
58
|
+
|
|
59
|
+
await db('customers').insert([
|
|
60
|
+
{
|
|
61
|
+
id: '00000000-0000-0000-0000-000000000001',
|
|
62
|
+
name: 'Alice',
|
|
63
|
+
created_at: new Date('2024-01-01'),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: '00000000-0000-0000-0000-000000000002',
|
|
67
|
+
name: 'Bob',
|
|
68
|
+
created_at: new Date('2024-01-02'),
|
|
69
|
+
},
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
await db('orders').insert([
|
|
73
|
+
{
|
|
74
|
+
id: '00000000-0000-0000-0000-000000000101',
|
|
75
|
+
customer_id: '00000000-0000-0000-0000-000000000001',
|
|
76
|
+
amount: 100,
|
|
77
|
+
created_at: new Date('2024-01-10'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: '00000000-0000-0000-0000-000000000102',
|
|
81
|
+
customer_id: '00000000-0000-0000-0000-000000000001',
|
|
82
|
+
amount: 200,
|
|
83
|
+
created_at: new Date('2024-01-11'),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: '00000000-0000-0000-0000-000000000103',
|
|
87
|
+
customer_id: '00000000-0000-0000-0000-000000000002',
|
|
88
|
+
amount: 300,
|
|
89
|
+
created_at: new Date('2024-01-12'),
|
|
90
|
+
},
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
class OrdersRepository extends BaseRepository {
|
|
95
|
+
static tableName = 'orders'
|
|
96
|
+
|
|
97
|
+
constructor(deps) {
|
|
98
|
+
super({
|
|
99
|
+
...deps,
|
|
100
|
+
baseQueryBuilder(qb) {
|
|
101
|
+
qb.innerJoin('customers', 'customers.id', 'orders.customer_id')
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe('BaseRepository integration', () => {
|
|
108
|
+
it('baseQueryBuilder applies JOIN correctly', async () => {
|
|
109
|
+
const repo = new OrdersRepository({ db })
|
|
110
|
+
|
|
111
|
+
const qb = repo.baseQuery()
|
|
112
|
+
|
|
113
|
+
const rows = await qb.select('orders.id', 'customers.name')
|
|
114
|
+
|
|
115
|
+
expect(rows).toHaveLength(3)
|
|
116
|
+
expect(rows[0]).toHaveProperty('name')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('find returns joined data using columns', async () => {
|
|
120
|
+
const repo = new OrdersRepository({ db })
|
|
121
|
+
|
|
122
|
+
const result = await repo.find({
|
|
123
|
+
columns: ['orders.id', 'orders.amount', 'customers.name'],
|
|
124
|
+
limit: 10,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(result.list.length).toBe(3)
|
|
128
|
+
|
|
129
|
+
const row = result.list[0]
|
|
130
|
+
|
|
131
|
+
expect(row).toHaveProperty('amount')
|
|
132
|
+
expect(row).toHaveProperty('name')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('pagination works with joins', async () => {
|
|
136
|
+
const repo = new OrdersRepository({ db })
|
|
137
|
+
|
|
138
|
+
const result = await repo.find({
|
|
139
|
+
columns: ['orders.id', 'customers.name'],
|
|
140
|
+
limit: 2,
|
|
141
|
+
page: 1,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(result.list.length).toBe(2)
|
|
145
|
+
expect(result.totalCount).toBe(3)
|
|
146
|
+
expect(result.pages).toBe(2)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('constructor default columns are used', async () => {
|
|
150
|
+
class RepoWithColumns extends BaseRepository {
|
|
151
|
+
static tableName = 'orders'
|
|
152
|
+
|
|
153
|
+
constructor(deps) {
|
|
154
|
+
super({
|
|
155
|
+
...deps,
|
|
156
|
+
columns: ['orders.id'],
|
|
157
|
+
baseQueryBuilder(qb) {
|
|
158
|
+
qb.innerJoin('customers', 'customers.id', 'orders.customer_id')
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const repo = new RepoWithColumns({ db })
|
|
165
|
+
|
|
166
|
+
const result = await repo.find({})
|
|
167
|
+
|
|
168
|
+
const row = result.list[0]
|
|
169
|
+
|
|
170
|
+
expect(Object.keys(row)).toEqual(['id'])
|
|
171
|
+
})
|
|
172
|
+
})
|
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { BaseRepository } from '../../src/postgresql/repositories/BaseRepository.js'
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// mock paginate
|
|
5
5
|
vi.mock('../../src/postgresql/pagination/paginate.js', () => ({
|
|
6
|
-
sqlPaginate: vi.fn()
|
|
6
|
+
sqlPaginate: vi.fn(),
|
|
7
7
|
}))
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
vi.mock('../../filters/apply-filter.js', () => ({
|
|
14
|
-
applyFilter: vi.fn(),
|
|
15
|
-
}))
|
|
16
|
-
|
|
17
|
-
vi.mock('../../core/normalize-premitives-types-or-default.js', () => ({
|
|
18
|
-
normalizeNumberOrDefault: vi.fn((v) => Number(v)),
|
|
9
|
+
// mock snakeCase mapper
|
|
10
|
+
vi.mock('../../src/core/case-mapper.js', () => ({
|
|
11
|
+
toSnakeCase: vi.fn((v) => v),
|
|
19
12
|
}))
|
|
20
13
|
|
|
21
14
|
// import AFTER mocks
|
|
@@ -24,6 +17,7 @@ import { toSnakeCase } from '../../src/core/case-mapper.js'
|
|
|
24
17
|
|
|
25
18
|
describe('BaseRepository', () => {
|
|
26
19
|
let dbMock
|
|
20
|
+
let qbMock
|
|
27
21
|
let logMock
|
|
28
22
|
|
|
29
23
|
class TestRepo extends BaseRepository {
|
|
@@ -31,13 +25,21 @@ describe('BaseRepository', () => {
|
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
beforeEach(() => {
|
|
34
|
-
|
|
35
|
-
clone: vi.fn(),
|
|
36
|
-
}
|
|
28
|
+
qbMock = {
|
|
29
|
+
clone: vi.fn().mockReturnThis(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dbMock = vi.fn(() => qbMock)
|
|
37
33
|
|
|
38
34
|
logMock = {
|
|
39
35
|
error: vi.fn(),
|
|
40
36
|
}
|
|
37
|
+
|
|
38
|
+
sqlPaginate.mockResolvedValue({ page: 1 })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
vi.clearAllMocks()
|
|
41
43
|
})
|
|
42
44
|
|
|
43
45
|
it('throws if db not provided', () => {
|
|
@@ -58,7 +60,7 @@ describe('BaseRepository', () => {
|
|
|
58
60
|
})
|
|
59
61
|
|
|
60
62
|
it('baseQuery uses trx when provided', () => {
|
|
61
|
-
const trxMock = vi.fn()
|
|
63
|
+
const trxMock = vi.fn(() => qbMock)
|
|
62
64
|
const repo = new TestRepo({ db: dbMock })
|
|
63
65
|
|
|
64
66
|
repo.baseQuery({ trx: trxMock })
|
|
@@ -68,7 +70,8 @@ describe('BaseRepository', () => {
|
|
|
68
70
|
})
|
|
69
71
|
|
|
70
72
|
it('baseQuery applies baseQueryBuilder', () => {
|
|
71
|
-
const builderMock = vi.fn(
|
|
73
|
+
const builderMock = vi.fn()
|
|
74
|
+
|
|
72
75
|
const repo = new TestRepo({
|
|
73
76
|
db: dbMock,
|
|
74
77
|
baseQueryBuilder: builderMock,
|
|
@@ -76,7 +79,7 @@ describe('BaseRepository', () => {
|
|
|
76
79
|
|
|
77
80
|
repo.baseQuery({}, { some: 'param' })
|
|
78
81
|
|
|
79
|
-
expect(builderMock).
|
|
82
|
+
expect(builderMock).toHaveBeenCalledWith(qbMock, { some: 'param' })
|
|
80
83
|
})
|
|
81
84
|
|
|
82
85
|
it('find calls sqlPaginate with correct params', async () => {
|
|
@@ -88,17 +91,65 @@ describe('BaseRepository', () => {
|
|
|
88
91
|
filter: { a: 1 },
|
|
89
92
|
})
|
|
90
93
|
|
|
91
|
-
expect(
|
|
94
|
+
expect(toSnakeCase).toHaveBeenCalledWith({ a: 1 })
|
|
95
|
+
|
|
96
|
+
expect(sqlPaginate).toHaveBeenCalledWith(
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
page: 2,
|
|
99
|
+
limit: 5,
|
|
100
|
+
baseQuery: qbMock,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
92
103
|
})
|
|
93
104
|
|
|
94
105
|
it('logs error if sqlPaginate throws', async () => {
|
|
95
|
-
// @ts-ignore
|
|
96
106
|
sqlPaginate.mockRejectedValueOnce(new Error('fail'))
|
|
97
107
|
|
|
98
108
|
const repo = new TestRepo({ db: dbMock, log: logMock })
|
|
99
109
|
|
|
100
|
-
await expect(repo.find({ filter: {} })).rejects.toThrow()
|
|
110
|
+
await expect(repo.find({ filter: {} })).rejects.toThrow('fail')
|
|
101
111
|
|
|
102
112
|
expect(logMock.error).toHaveBeenCalled()
|
|
103
113
|
})
|
|
114
|
+
|
|
115
|
+
it('find uses columns passed to find()', async () => {
|
|
116
|
+
const repo = new TestRepo({ db: dbMock })
|
|
117
|
+
|
|
118
|
+
await repo.find({
|
|
119
|
+
columns: ['id', 'name'],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(sqlPaginate).toHaveBeenCalledWith(
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
columns: ['id', 'name'],
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('find falls back to constructor columns', async () => {
|
|
130
|
+
const repo = new TestRepo({
|
|
131
|
+
db: dbMock,
|
|
132
|
+
columns: ['id', 'email'],
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
await repo.find({})
|
|
136
|
+
|
|
137
|
+
expect(sqlPaginate).toHaveBeenCalledWith(
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
columns: ['id', 'email'],
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('find falls back to "*" when no columns provided', async () => {
|
|
145
|
+
const repo = new TestRepo({ db: dbMock })
|
|
146
|
+
|
|
147
|
+
await repo.find({})
|
|
148
|
+
|
|
149
|
+
expect(sqlPaginate).toHaveBeenCalledWith(
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
columns: '*',
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
})
|
|
104
155
|
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
|
|
3
|
+
import knex from 'knex'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
stopPostgres,
|
|
7
|
+
startPostgres,
|
|
8
|
+
buildPostgresUri,
|
|
9
|
+
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
|
+
|
|
11
|
+
import { TenantScopedRepository } from '../../src/postgresql/repositories/TenantScopedRepository.js'
|
|
12
|
+
|
|
13
|
+
const PG_OPTIONS = {
|
|
14
|
+
port: 5446,
|
|
15
|
+
db: 'testdb',
|
|
16
|
+
user: 'testuser',
|
|
17
|
+
pass: 'testpass',
|
|
18
|
+
containerName: 'postgres-tenant-repo-test-5446',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
|
|
22
|
+
|
|
23
|
+
const TENANT_A = '00000000-0000-0000-0000-000000000010'
|
|
24
|
+
const TENANT_B = '00000000-0000-0000-0000-000000000020'
|
|
25
|
+
|
|
26
|
+
let db
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
startPostgres(PG_OPTIONS)
|
|
30
|
+
|
|
31
|
+
db = knex({
|
|
32
|
+
client: 'pg',
|
|
33
|
+
connection: DATABASE_URI,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await db.schema.createTable('customers', (table) => {
|
|
37
|
+
table.uuid('id').primary()
|
|
38
|
+
table.uuid('tenant_id').notNullable()
|
|
39
|
+
table.string('name')
|
|
40
|
+
table.timestamp('created_at').notNullable()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await db.schema.createTable('orders', (table) => {
|
|
44
|
+
table.uuid('id').primary()
|
|
45
|
+
table.uuid('tenant_id').notNullable()
|
|
46
|
+
table.uuid('customer_id')
|
|
47
|
+
table.decimal('amount', 10, 2)
|
|
48
|
+
table.timestamp('created_at').notNullable()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
if (db) {
|
|
54
|
+
await db.destroy()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
stopPostgres(PG_OPTIONS.containerName)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
await db('orders').truncate()
|
|
62
|
+
await db('customers').truncate()
|
|
63
|
+
|
|
64
|
+
await db('customers').insert([
|
|
65
|
+
{
|
|
66
|
+
id: '00000000-0000-0000-0000-000000000001',
|
|
67
|
+
tenant_id: TENANT_A,
|
|
68
|
+
name: 'Alice',
|
|
69
|
+
created_at: new Date('2024-01-01'),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: '00000000-0000-0000-0000-000000000002',
|
|
73
|
+
tenant_id: TENANT_B,
|
|
74
|
+
name: 'Bob',
|
|
75
|
+
created_at: new Date('2024-01-02'),
|
|
76
|
+
},
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
await db('orders').insert([
|
|
80
|
+
{
|
|
81
|
+
id: '00000000-0000-0000-0000-000000000101',
|
|
82
|
+
tenant_id: TENANT_A,
|
|
83
|
+
customer_id: '00000000-0000-0000-0000-000000000001',
|
|
84
|
+
amount: 100,
|
|
85
|
+
created_at: new Date('2024-01-10'),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: '00000000-0000-0000-0000-000000000102',
|
|
89
|
+
tenant_id: TENANT_A,
|
|
90
|
+
customer_id: '00000000-0000-0000-0000-000000000001',
|
|
91
|
+
amount: 200,
|
|
92
|
+
created_at: new Date('2024-01-11'),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: '00000000-0000-0000-0000-000000000103',
|
|
96
|
+
tenant_id: TENANT_B,
|
|
97
|
+
customer_id: '00000000-0000-0000-0000-000000000002',
|
|
98
|
+
amount: 300,
|
|
99
|
+
created_at: new Date('2024-01-12'),
|
|
100
|
+
},
|
|
101
|
+
])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
class OrdersRepository extends TenantScopedRepository {
|
|
105
|
+
static tableName = 'orders'
|
|
106
|
+
|
|
107
|
+
constructor(deps) {
|
|
108
|
+
super({
|
|
109
|
+
...deps,
|
|
110
|
+
baseQueryBuilder(qb) {
|
|
111
|
+
qb.innerJoin('customers', 'customers.id', 'orders.customer_id')
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('TenantScopedRepository integration', () => {
|
|
118
|
+
it('throws if tenantId missing', async () => {
|
|
119
|
+
const repo = new OrdersRepository({ db })
|
|
120
|
+
|
|
121
|
+
await expect(
|
|
122
|
+
repo.find({
|
|
123
|
+
filter: {},
|
|
124
|
+
}),
|
|
125
|
+
).rejects.toThrow('tenantId is required')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('returns only rows for tenant', async () => {
|
|
129
|
+
const repo = new OrdersRepository({ db })
|
|
130
|
+
|
|
131
|
+
const result = await repo.find({
|
|
132
|
+
filter: {
|
|
133
|
+
tenantId: TENANT_A,
|
|
134
|
+
},
|
|
135
|
+
columns: ['orders.id', 'orders.amount'],
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
expect(result.list.length).toBe(2)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('works with join and columns', async () => {
|
|
142
|
+
const repo = new OrdersRepository({ db })
|
|
143
|
+
|
|
144
|
+
const result = await repo.find({
|
|
145
|
+
filter: {
|
|
146
|
+
tenantId: TENANT_A,
|
|
147
|
+
},
|
|
148
|
+
columns: ['orders.id', 'orders.amount', 'customers.name'],
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const row = result.list[0]
|
|
152
|
+
|
|
153
|
+
expect(row).toHaveProperty('amount')
|
|
154
|
+
expect(row).toHaveProperty('name')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('pagination respects tenant scope', async () => {
|
|
158
|
+
const repo = new OrdersRepository({ db })
|
|
159
|
+
|
|
160
|
+
const result = await repo.find({
|
|
161
|
+
filter: {
|
|
162
|
+
tenantId: TENANT_A,
|
|
163
|
+
},
|
|
164
|
+
limit: 1,
|
|
165
|
+
page: 1,
|
|
166
|
+
columns: ['orders.id'],
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
expect(result.list.length).toBe(1)
|
|
170
|
+
expect(result.totalCount).toBe(2)
|
|
171
|
+
expect(result.pages).toBe(2)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Does NOT enforce tenant isolation.
|
|
11
11
|
*/
|
|
12
12
|
export class BaseRepository {
|
|
13
|
-
constructor({ db, log, baseQueryBuilder }?: {})
|
|
13
|
+
constructor({ db, log, columns, baseQueryBuilder }?: {})
|
|
14
14
|
db: any
|
|
15
15
|
log: any
|
|
16
|
+
_columns: any
|
|
16
17
|
_baseQueryBuilder: any
|
|
17
18
|
/**
|
|
18
19
|
* Each concrete repository must define:
|
|
@@ -29,22 +30,24 @@ export class BaseRepository {
|
|
|
29
30
|
*/
|
|
30
31
|
find({
|
|
31
32
|
page,
|
|
33
|
+
columns,
|
|
32
34
|
limit,
|
|
33
35
|
filter,
|
|
34
|
-
orderBy,
|
|
35
36
|
options,
|
|
36
37
|
mapRow,
|
|
38
|
+
orderBy,
|
|
37
39
|
...restParams
|
|
38
40
|
}: {
|
|
39
41
|
[x: string]: any
|
|
40
42
|
page?: number
|
|
43
|
+
columns: any
|
|
41
44
|
limit?: number
|
|
42
45
|
filter?: {}
|
|
46
|
+
options?: {}
|
|
47
|
+
mapRow?: (row: any) => any
|
|
43
48
|
orderBy?: {
|
|
44
49
|
column: string
|
|
45
50
|
direction: string
|
|
46
51
|
}
|
|
47
|
-
options?: {}
|
|
48
|
-
mapRow?: (row: any) => any
|
|
49
52
|
}): Promise<any>
|
|
50
53
|
}
|