core-services-sdk 1.3.71 → 1.3.73
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/modifiers/apply-order-by.js +32 -6
- package/src/postgresql/validate-schema.js +23 -14
- package/tests/postgresql/modifiers/apply-order-by.integration.test.js +152 -0
- package/tests/postgresql/modifiers/{apply-order-by.test.js → apply-order-by.unit.test.js} +67 -0
package/package.json
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import { getTableNameFromQuery } from '../core/get-table-name.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* @typedef {Object} OrderByItem
|
|
5
|
+
* @property {string} column - Column name to order by
|
|
6
|
+
* @property {'asc' | 'desc'} [direction='asc'] - Order direction
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const ALLOWED_DIRECTIONS = ['asc', 'desc']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Applies ORDER BY clause(s) to a Knex query builder.
|
|
13
|
+
*
|
|
14
|
+
* Supports a single orderBy object or an array of orderBy objects.
|
|
15
|
+
* Validates order direction to prevent invalid SQL.
|
|
5
16
|
*
|
|
6
17
|
* @param {Object} params
|
|
7
|
-
* @param {import('knex').Knex.QueryBuilder} params.query
|
|
8
|
-
* @param {
|
|
18
|
+
* @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance
|
|
19
|
+
* @param {OrderByItem | OrderByItem[]} params.orderBy - Order by definition(s)
|
|
20
|
+
* @returns {import('knex').Knex.QueryBuilder} The modified query builder
|
|
9
21
|
*/
|
|
10
22
|
export function applyOrderBy({ query, orderBy }) {
|
|
11
|
-
if (!orderBy
|
|
23
|
+
if (!orderBy) {
|
|
12
24
|
return query
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
const tableName = getTableNameFromQuery(query)
|
|
16
|
-
const
|
|
28
|
+
const orderByArray = [].concat(orderBy)
|
|
29
|
+
|
|
30
|
+
return orderByArray.reduce((query, item) => {
|
|
31
|
+
if (!item?.column) {
|
|
32
|
+
return query
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const direction = item.direction || 'asc'
|
|
36
|
+
|
|
37
|
+
if (!ALLOWED_DIRECTIONS.includes(direction)) {
|
|
38
|
+
throw new Error(`Invalid order direction: ${direction}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const column = tableName ? `${tableName}.${item.column}` : item.column
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
return query.orderBy(column, direction)
|
|
44
|
+
}, query)
|
|
19
45
|
}
|
|
@@ -35,24 +35,33 @@ export async function validateSchema({
|
|
|
35
35
|
connection,
|
|
36
36
|
log = { info: console.info },
|
|
37
37
|
}) {
|
|
38
|
-
const db = connectToPg(connection
|
|
38
|
+
const db = connectToPg(connection, {
|
|
39
|
+
pool: {
|
|
40
|
+
min: 0,
|
|
41
|
+
max: 1,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
try {
|
|
46
|
+
const missingTables = []
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
for (const table of tables) {
|
|
49
|
+
const exists = await db.schema.hasTable(table)
|
|
50
|
+
if (!exists) {
|
|
51
|
+
missingTables.push(table)
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
|
-
}
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if (missingTables.length > 0) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Missing the following tables: ${missingTables.join(', ')}. Did you run migrations?`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
if (tables.length) {
|
|
62
|
+
log.info(`All required tables are exists: ${tables.join(', ')}`)
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await db.destroy()
|
|
57
66
|
}
|
|
58
67
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
|
|
3
|
+
import knex from 'knex'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
startPostgres,
|
|
7
|
+
stopPostgres,
|
|
8
|
+
buildPostgresUri,
|
|
9
|
+
} from '../../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
|
+
|
|
11
|
+
import { applyOrderBy } from '../../../src/postgresql/modifiers/apply-order-by.js'
|
|
12
|
+
|
|
13
|
+
const PG_OPTIONS = {
|
|
14
|
+
port: 5447,
|
|
15
|
+
db: 'testdb',
|
|
16
|
+
user: 'testuser',
|
|
17
|
+
pass: 'testpass',
|
|
18
|
+
containerName: 'postgres-apply-order-by-test-5447',
|
|
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('tenants', (table) => {
|
|
34
|
+
table.uuid('id').primary()
|
|
35
|
+
table.string('name').notNullable()
|
|
36
|
+
table.string('type').notNullable()
|
|
37
|
+
table.integer('age').notNullable()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterAll(async () => {
|
|
42
|
+
if (db) {
|
|
43
|
+
await db.destroy()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stopPostgres(PG_OPTIONS.containerName)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
await db('tenants').truncate()
|
|
51
|
+
|
|
52
|
+
await db('tenants').insert([
|
|
53
|
+
{
|
|
54
|
+
id: '00000000-0000-0000-0000-000000000001',
|
|
55
|
+
name: 'Tenant A',
|
|
56
|
+
type: 'business',
|
|
57
|
+
age: 2,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: '00000000-0000-0000-0000-000000000002',
|
|
61
|
+
name: 'Tenant B',
|
|
62
|
+
type: 'business',
|
|
63
|
+
age: 5,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: '00000000-0000-0000-0000-000000000003',
|
|
67
|
+
name: 'Tenant C',
|
|
68
|
+
type: 'cpa',
|
|
69
|
+
age: 7,
|
|
70
|
+
},
|
|
71
|
+
])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('applyOrderBy (integration)', () => {
|
|
75
|
+
it('orders by column ascending by default', async () => {
|
|
76
|
+
const query = db('tenants')
|
|
77
|
+
|
|
78
|
+
applyOrderBy({
|
|
79
|
+
query,
|
|
80
|
+
orderBy: { column: 'age' },
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const ages = (await query.select('*')).map((r) => r.age)
|
|
84
|
+
expect(ages).toEqual([2, 5, 7])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('orders by column descending', async () => {
|
|
88
|
+
const query = db('tenants')
|
|
89
|
+
|
|
90
|
+
applyOrderBy({
|
|
91
|
+
query,
|
|
92
|
+
orderBy: { column: 'age', direction: 'desc' },
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const ages = (await query.select('*')).map((r) => r.age)
|
|
96
|
+
expect(ages).toEqual([7, 5, 2])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('supports multiple ORDER BY clauses', async () => {
|
|
100
|
+
const query = db('tenants')
|
|
101
|
+
|
|
102
|
+
applyOrderBy({
|
|
103
|
+
query,
|
|
104
|
+
orderBy: [
|
|
105
|
+
{ column: 'type', direction: 'asc' },
|
|
106
|
+
{ column: 'age', direction: 'desc' },
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const result = await query.select('*')
|
|
111
|
+
|
|
112
|
+
expect(result.map((r) => `${r.type}-${r.age}`)).toEqual([
|
|
113
|
+
'business-5',
|
|
114
|
+
'business-2',
|
|
115
|
+
'cpa-7',
|
|
116
|
+
])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('does nothing when orderBy is missing', async () => {
|
|
120
|
+
const query = db('tenants')
|
|
121
|
+
|
|
122
|
+
applyOrderBy({ query })
|
|
123
|
+
|
|
124
|
+
const rows = await query.select('*')
|
|
125
|
+
expect(rows.length).toBe(3)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('throws error for invalid order direction', async () => {
|
|
129
|
+
const query = db('tenants')
|
|
130
|
+
|
|
131
|
+
expect(() =>
|
|
132
|
+
applyOrderBy({
|
|
133
|
+
query,
|
|
134
|
+
orderBy: { column: 'age', direction: 'sideways' },
|
|
135
|
+
}),
|
|
136
|
+
).toThrow('Invalid order direction: sideways')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('does not execute query when invalid direction is provided', async () => {
|
|
140
|
+
const query = db('tenants')
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
applyOrderBy({
|
|
144
|
+
query,
|
|
145
|
+
orderBy: { column: 'age', direction: 'up' },
|
|
146
|
+
})
|
|
147
|
+
} catch {}
|
|
148
|
+
|
|
149
|
+
const rows = await db('tenants').select('*')
|
|
150
|
+
expect(rows.length).toBe(3)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -51,6 +51,73 @@ describe('applyOrderBy', () => {
|
|
|
51
51
|
expect(query.orderBy).toHaveBeenCalledWith('assets.created_at', 'desc')
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
it('throws an error for invalid order direction', () => {
|
|
55
|
+
const query = {
|
|
56
|
+
orderBy: vi.fn(),
|
|
57
|
+
_single: { table: 'assets' },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
expect(() =>
|
|
61
|
+
applyOrderBy({
|
|
62
|
+
query,
|
|
63
|
+
orderBy: { column: 'created_at', direction: 'up' },
|
|
64
|
+
}),
|
|
65
|
+
).toThrow('Invalid order direction: up')
|
|
66
|
+
|
|
67
|
+
expect(query.orderBy).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('falls back to asc when direction is undefined', () => {
|
|
71
|
+
const query = {
|
|
72
|
+
orderBy: vi.fn(() => query),
|
|
73
|
+
_single: { table: 'assets' },
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
applyOrderBy({
|
|
77
|
+
query,
|
|
78
|
+
orderBy: { column: 'created_at', direction: undefined },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(query.orderBy).toHaveBeenCalledWith('assets.created_at', 'asc')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('applies multiple ORDER BY clauses from array', () => {
|
|
85
|
+
const query = {
|
|
86
|
+
orderBy: vi.fn(() => query),
|
|
87
|
+
_single: { table: 'assets' },
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
applyOrderBy({
|
|
91
|
+
query,
|
|
92
|
+
orderBy: [{ column: 'created_at', direction: 'desc' }, { column: 'id' }],
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(query.orderBy).toHaveBeenNthCalledWith(
|
|
96
|
+
1,
|
|
97
|
+
'assets.created_at',
|
|
98
|
+
'desc',
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(query.orderBy).toHaveBeenNthCalledWith(2, 'assets.id', 'asc')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('throws on invalid direction inside orderBy array', () => {
|
|
105
|
+
const query = {
|
|
106
|
+
orderBy: vi.fn(() => query),
|
|
107
|
+
_single: { table: 'assets' },
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
expect(() =>
|
|
111
|
+
applyOrderBy({
|
|
112
|
+
query,
|
|
113
|
+
orderBy: [
|
|
114
|
+
{ column: 'created_at', direction: 'desc' },
|
|
115
|
+
{ column: 'id', direction: 'sideways' },
|
|
116
|
+
],
|
|
117
|
+
}),
|
|
118
|
+
).toThrow('Invalid order direction: sideways')
|
|
119
|
+
})
|
|
120
|
+
|
|
54
121
|
it('uses unqualified column when table name cannot be resolved', () => {
|
|
55
122
|
const query = {
|
|
56
123
|
orderBy: vi.fn(() => query),
|