core-services-sdk 1.3.64 → 1.3.65
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/core/get-table-name.js +10 -0
- package/src/postgresql/filters/apply-filter-object.js +19 -0
- package/src/postgresql/filters/apply-filter-snake-case.js +16 -0
- package/src/postgresql/filters/apply-filter.js +33 -0
- package/src/postgresql/filters/operators.js +32 -0
- package/src/postgresql/index.js +5 -2
- package/src/postgresql/modifiers/apply-order-by.js +19 -0
- package/src/postgresql/modifiers/apply-pagination.js +12 -0
- package/src/postgresql/pagination/paginate.js +48 -0
- package/tests/core/normalize-phone-number.unit.test.js +1 -1
- package/tests/postgresql/apply-filter-snake-case.integration.test.js +25 -354
- package/tests/postgresql/apply-filter.integration.test.js +28 -78
- package/tests/postgresql/core/get-table-name.unit.test.js +20 -0
- package/tests/postgresql/filters/apply-filter-object.test.js +23 -0
- package/tests/postgresql/filters/operators.unit.test.js +23 -0
- package/tests/postgresql/modifiers/apply-order-by.test.js +80 -0
- package/tests/postgresql/modifiers/apply-pagination.unit.test.js +18 -0
- package/tests/postgresql/paginate.integration.test.js +8 -16
- package/tests/postgresql/pagination/paginate.js +48 -0
- package/tests/postgresql/validate-schema.integration.test.js +7 -3
- package/types/postgresql/core/get-table-name.d.ts +10 -0
- package/types/postgresql/filters/apply-filter-object.d.ts +15 -0
- package/types/postgresql/filters/apply-filter-snake-case.d.ts +14 -0
- package/types/postgresql/filters/apply-filter.d.ts +15 -0
- package/types/postgresql/filters/operators.d.ts +14 -0
- package/types/postgresql/index.d.ts +5 -2
- package/types/postgresql/modifiers/apply-order-by.d.ts +17 -0
- package/types/postgresql/modifiers/apply-pagination.d.ts +17 -0
- package/types/postgresql/pagination/paginate.d.ts +29 -0
- package/src/postgresql/apply-filter.js +0 -401
- package/src/postgresql/paginate.js +0 -61
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { OPERATORS } from '../../../src/postgresql/filters/operators.js'
|
|
4
|
+
|
|
5
|
+
describe('OPERATORS', () => {
|
|
6
|
+
it('eq calls where with "="', () => {
|
|
7
|
+
const q = { where: vi.fn(() => q) }
|
|
8
|
+
OPERATORS.eq(q, 'a', 1)
|
|
9
|
+
expect(q.where).toHaveBeenCalledWith('a', '=', 1)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('in calls whereIn', () => {
|
|
13
|
+
const q = { whereIn: vi.fn(() => q) }
|
|
14
|
+
OPERATORS.in(q, 'a', [1, 2])
|
|
15
|
+
expect(q.whereIn).toHaveBeenCalledWith('a', [1, 2])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('isNull true calls whereNull', () => {
|
|
19
|
+
const q = { whereNull: vi.fn(() => q) }
|
|
20
|
+
OPERATORS.isNull(q, 'a', true)
|
|
21
|
+
expect(q.whereNull).toHaveBeenCalledWith('a')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
import { applyOrderBy } from '../../../src/postgresql/modifiers/apply-order-by.js'
|
|
4
|
+
|
|
5
|
+
describe('applyOrderBy', () => {
|
|
6
|
+
it('does nothing when orderBy is not provided', () => {
|
|
7
|
+
const query = {}
|
|
8
|
+
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
const result = applyOrderBy({ query })
|
|
11
|
+
|
|
12
|
+
expect(result).toBe(query)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('does nothing when orderBy.column is missing', () => {
|
|
16
|
+
const query = {}
|
|
17
|
+
|
|
18
|
+
const result = applyOrderBy({
|
|
19
|
+
query,
|
|
20
|
+
orderBy: {},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(result).toBe(query)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('applies ORDER BY with default direction asc', () => {
|
|
27
|
+
const query = {
|
|
28
|
+
orderBy: vi.fn(() => query),
|
|
29
|
+
_single: { table: 'assets' },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
applyOrderBy({
|
|
33
|
+
query,
|
|
34
|
+
orderBy: { column: 'created_at' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(query.orderBy).toHaveBeenCalledWith('assets.created_at', 'asc')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('applies ORDER BY with explicit direction', () => {
|
|
41
|
+
const query = {
|
|
42
|
+
orderBy: vi.fn(() => query),
|
|
43
|
+
_single: { table: 'assets' },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
applyOrderBy({
|
|
47
|
+
query,
|
|
48
|
+
orderBy: { column: 'created_at', direction: 'desc' },
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(query.orderBy).toHaveBeenCalledWith('assets.created_at', 'desc')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('uses unqualified column when table name cannot be resolved', () => {
|
|
55
|
+
const query = {
|
|
56
|
+
orderBy: vi.fn(() => query),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
applyOrderBy({
|
|
60
|
+
query,
|
|
61
|
+
orderBy: { column: 'created_at' },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(query.orderBy).toHaveBeenCalledWith('created_at', 'asc')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('returns the same query instance for chaining', () => {
|
|
68
|
+
const query = {
|
|
69
|
+
orderBy: vi.fn(() => query),
|
|
70
|
+
_single: { table: 'assets' },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = applyOrderBy({
|
|
74
|
+
query,
|
|
75
|
+
orderBy: { column: 'id' },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(result).toBe(query)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { applyPagination } from '../../../src/postgresql/modifiers/apply-pagination.js'
|
|
4
|
+
|
|
5
|
+
describe('applyPagination', () => {
|
|
6
|
+
it('applies limit and offset', () => {
|
|
7
|
+
const q = {
|
|
8
|
+
limit: vi.fn(() => q),
|
|
9
|
+
offset: vi.fn(() => q),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
applyPagination({ query: q, page: 2, limit: 10 })
|
|
14
|
+
|
|
15
|
+
expect(q.limit).toHaveBeenCalledWith(10)
|
|
16
|
+
expect(q.offset).toHaveBeenCalledWith(10)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
buildPostgresUri,
|
|
9
9
|
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
10
|
|
|
11
|
-
import { sqlPaginate } from '../../src/postgresql/paginate.js'
|
|
11
|
+
import { sqlPaginate } from '../../src/postgresql/pagination/paginate.js'
|
|
12
12
|
|
|
13
13
|
const PG_OPTIONS = {
|
|
14
14
|
port: 5442,
|
|
@@ -73,8 +73,7 @@ beforeEach(async () => {
|
|
|
73
73
|
describe('paginate integration', () => {
|
|
74
74
|
it('returns first page without ordering guarantees', async () => {
|
|
75
75
|
const result = await sqlPaginate({
|
|
76
|
-
db,
|
|
77
|
-
tableName: 'tenants',
|
|
76
|
+
baseQuery: db('tenants'),
|
|
78
77
|
page: 1,
|
|
79
78
|
limit: 2,
|
|
80
79
|
})
|
|
@@ -84,14 +83,12 @@ describe('paginate integration', () => {
|
|
|
84
83
|
expect(result.currentPage).toBe(1)
|
|
85
84
|
expect(result.hasPrevious).toBe(false)
|
|
86
85
|
expect(result.hasNext).toBe(true)
|
|
87
|
-
|
|
88
86
|
expect(result.list).toHaveLength(2)
|
|
89
87
|
})
|
|
90
88
|
|
|
91
89
|
it('returns second page without ordering guarantees', async () => {
|
|
92
90
|
const result = await sqlPaginate({
|
|
93
|
-
db,
|
|
94
|
-
tableName: 'tenants',
|
|
91
|
+
baseQuery: db('tenants'),
|
|
95
92
|
page: 2,
|
|
96
93
|
limit: 2,
|
|
97
94
|
})
|
|
@@ -101,14 +98,12 @@ describe('paginate integration', () => {
|
|
|
101
98
|
expect(result.currentPage).toBe(2)
|
|
102
99
|
expect(result.hasPrevious).toBe(true)
|
|
103
100
|
expect(result.hasNext).toBe(false)
|
|
104
|
-
|
|
105
101
|
expect(result.list).toHaveLength(1)
|
|
106
102
|
})
|
|
107
103
|
|
|
108
|
-
it('applies filters correctly
|
|
104
|
+
it('applies filters correctly', async () => {
|
|
109
105
|
const result = await sqlPaginate({
|
|
110
|
-
db,
|
|
111
|
-
tableName: 'tenants',
|
|
106
|
+
baseQuery: db('tenants'),
|
|
112
107
|
filter: { type: 'business' },
|
|
113
108
|
limit: 10,
|
|
114
109
|
})
|
|
@@ -121,8 +116,7 @@ describe('paginate integration', () => {
|
|
|
121
116
|
|
|
122
117
|
it('supports custom ordering', async () => {
|
|
123
118
|
const result = await sqlPaginate({
|
|
124
|
-
db,
|
|
125
|
-
tableName: 'tenants',
|
|
119
|
+
baseQuery: db('tenants'),
|
|
126
120
|
orderBy: {
|
|
127
121
|
column: 'created_at',
|
|
128
122
|
direction: 'asc',
|
|
@@ -138,8 +132,7 @@ describe('paginate integration', () => {
|
|
|
138
132
|
|
|
139
133
|
it('supports row mapping', async () => {
|
|
140
134
|
const result = await sqlPaginate({
|
|
141
|
-
db,
|
|
142
|
-
tableName: 'tenants',
|
|
135
|
+
baseQuery: db('tenants'),
|
|
143
136
|
mapRow: (row) => ({
|
|
144
137
|
...row,
|
|
145
138
|
mapped: true,
|
|
@@ -152,8 +145,7 @@ describe('paginate integration', () => {
|
|
|
152
145
|
|
|
153
146
|
it('returns empty list when no records match filter', async () => {
|
|
154
147
|
const result = await sqlPaginate({
|
|
155
|
-
db,
|
|
156
|
-
tableName: 'tenants',
|
|
148
|
+
baseQuery: db('tenants'),
|
|
157
149
|
filter: { type: 'non-existing' },
|
|
158
150
|
})
|
|
159
151
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { applyFilter } from '../filters/apply-filter.js'
|
|
2
|
+
import { applyFilterSnakeCase } from '../filters/apply-filter-snake-case.js'
|
|
3
|
+
import { applyOrderBy } from '../modifiers/apply-order-by.js'
|
|
4
|
+
import { applyPagination } from '../modifiers/apply-pagination.js'
|
|
5
|
+
import { normalizeNumberOrDefault } from '../../core/normalize-premitives-types-or-default.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Executes paginated query.
|
|
9
|
+
*
|
|
10
|
+
* @async
|
|
11
|
+
*/
|
|
12
|
+
export async function sqlPaginate({
|
|
13
|
+
mapRow,
|
|
14
|
+
orderBy,
|
|
15
|
+
page = 1,
|
|
16
|
+
baseQuery,
|
|
17
|
+
limit = 10,
|
|
18
|
+
filter = {},
|
|
19
|
+
snakeCase = true,
|
|
20
|
+
}) {
|
|
21
|
+
const listQuery = baseQuery.clone()
|
|
22
|
+
const countQuery = baseQuery.clone()
|
|
23
|
+
|
|
24
|
+
const applyFilterFn = snakeCase ? applyFilterSnakeCase : applyFilter
|
|
25
|
+
|
|
26
|
+
applyFilterFn({ query: listQuery, filter })
|
|
27
|
+
applyFilterFn({ query: countQuery, filter })
|
|
28
|
+
|
|
29
|
+
applyOrderBy({ query: listQuery, orderBy })
|
|
30
|
+
applyPagination({ query: listQuery, page, limit })
|
|
31
|
+
|
|
32
|
+
const [rows, countResult] = await Promise.all([
|
|
33
|
+
listQuery.select('*'),
|
|
34
|
+
countQuery.count('* as count').first(),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const totalCount = normalizeNumberOrDefault(countResult?.count || 0)
|
|
38
|
+
const totalPages = Math.ceil(totalCount / limit)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
totalCount,
|
|
42
|
+
totalPages,
|
|
43
|
+
currentPage: page,
|
|
44
|
+
hasPrevious: page > 1,
|
|
45
|
+
hasNext: page < totalPages,
|
|
46
|
+
list: mapRow ? rows.map(mapRow) : rows,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
stopPostgres,
|
|
6
6
|
buildPostgresUri,
|
|
7
7
|
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
8
|
+
|
|
8
9
|
import { connectToPg } from '../../src/postgresql/connect-to-pg.js'
|
|
9
10
|
import { validateSchema } from '../../src/postgresql/validate-schema.js'
|
|
10
11
|
|
|
@@ -18,7 +19,7 @@ const PG_OPTIONS = {
|
|
|
18
19
|
|
|
19
20
|
const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
|
|
20
21
|
|
|
21
|
-
describe('validateSchema', () => {
|
|
22
|
+
describe('validateSchema integration', () => {
|
|
22
23
|
beforeAll(async () => {
|
|
23
24
|
startPostgres(PG_OPTIONS)
|
|
24
25
|
|
|
@@ -32,13 +33,16 @@ describe('validateSchema', () => {
|
|
|
32
33
|
await db.destroy()
|
|
33
34
|
})
|
|
34
35
|
|
|
35
|
-
afterAll(() => {
|
|
36
|
+
afterAll(async () => {
|
|
36
37
|
stopPostgres(PG_OPTIONS.containerName)
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
it('does not throw when all required tables exist', async () => {
|
|
40
41
|
await expect(
|
|
41
|
-
validateSchema({
|
|
42
|
+
validateSchema({
|
|
43
|
+
connection: DATABASE_URI,
|
|
44
|
+
tables: ['files'],
|
|
45
|
+
}),
|
|
42
46
|
).resolves.not.toThrow()
|
|
43
47
|
})
|
|
44
48
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts base table name from Knex QueryBuilder.
|
|
3
|
+
* Relies on Knex internal structure.
|
|
4
|
+
*
|
|
5
|
+
* @param {import('knex').Knex.QueryBuilder} query
|
|
6
|
+
* @returns {string|undefined}
|
|
7
|
+
*/
|
|
8
|
+
export function getTableNameFromQuery(
|
|
9
|
+
query: import('knex').Knex.QueryBuilder,
|
|
10
|
+
): string | undefined
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies multiple operators on the same column.
|
|
3
|
+
*
|
|
4
|
+
* @param {import('knex').Knex.QueryBuilder} query
|
|
5
|
+
* @param {string} qualifiedKey
|
|
6
|
+
* @param {Object<string, *>} value
|
|
7
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
8
|
+
*/
|
|
9
|
+
export function applyFilterObject(
|
|
10
|
+
query: import('knex').Knex.QueryBuilder,
|
|
11
|
+
qualifiedKey: string,
|
|
12
|
+
value: {
|
|
13
|
+
[x: string]: any
|
|
14
|
+
},
|
|
15
|
+
): import('knex').Knex.QueryBuilder
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies filters with automatic camelCase to snake_case conversion.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} params
|
|
5
|
+
* @param {import('knex').Knex.QueryBuilder} params.query
|
|
6
|
+
* @param {Object} params.filter
|
|
7
|
+
*/
|
|
8
|
+
export function applyFilterSnakeCase({
|
|
9
|
+
query,
|
|
10
|
+
filter,
|
|
11
|
+
}: {
|
|
12
|
+
query: import('knex').Knex.QueryBuilder
|
|
13
|
+
filter: any
|
|
14
|
+
}): import('knex').Knex.QueryBuilder<any, any>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies MongoDB-style filters to a Knex QueryBuilder.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} params
|
|
5
|
+
* @param {import('knex').Knex.QueryBuilder} params.query
|
|
6
|
+
* @param {Object} [params.filter]
|
|
7
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
8
|
+
*/
|
|
9
|
+
export function applyFilter({
|
|
10
|
+
query,
|
|
11
|
+
filter,
|
|
12
|
+
}: {
|
|
13
|
+
query: import('knex').Knex.QueryBuilder
|
|
14
|
+
filter?: any
|
|
15
|
+
}): import('knex').Knex.QueryBuilder
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Function} OperatorFunction
|
|
3
|
+
* @param {import('knex').Knex.QueryBuilder} q
|
|
4
|
+
* @param {string} key
|
|
5
|
+
* @param {*} value
|
|
6
|
+
* @returns {import('knex').Knex.QueryBuilder}
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* @type {Object<string, OperatorFunction>}
|
|
10
|
+
*/
|
|
11
|
+
export const OPERATORS: {
|
|
12
|
+
[x: string]: Function
|
|
13
|
+
}
|
|
14
|
+
export type OperatorFunction = Function
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export * from './paginate.js'
|
|
2
|
-
export * from './apply-filter.js'
|
|
3
1
|
export * from './connect-to-pg.js'
|
|
4
2
|
export * from './validate-schema.js'
|
|
3
|
+
export * from './pagination/paginate.js'
|
|
4
|
+
export * from './filters/apply-filter.js'
|
|
5
|
+
export * from './modifiers/apply-order-by.js'
|
|
5
6
|
export * from './start-stop-postgres-docker.js'
|
|
7
|
+
export * from './modifiers/apply-pagination.js'
|
|
8
|
+
export * from './filters/apply-filter-snake-case.js'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies ORDER BY clause.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} params
|
|
5
|
+
* @param {import('knex').Knex.QueryBuilder} params.query
|
|
6
|
+
* @param {{ column: string, direction?: 'asc'|'desc' }} params.orderBy
|
|
7
|
+
*/
|
|
8
|
+
export function applyOrderBy({
|
|
9
|
+
query,
|
|
10
|
+
orderBy,
|
|
11
|
+
}: {
|
|
12
|
+
query: import('knex').Knex.QueryBuilder
|
|
13
|
+
orderBy: {
|
|
14
|
+
column: string
|
|
15
|
+
direction?: 'asc' | 'desc'
|
|
16
|
+
}
|
|
17
|
+
}): import('knex').Knex.QueryBuilder<any, any>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies LIMIT and OFFSET.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} params
|
|
5
|
+
* @param {import('knex').Knex.QueryBuilder} params.query
|
|
6
|
+
* @param {number} [params.page=1]
|
|
7
|
+
* @param {number} [params.limit=10]
|
|
8
|
+
*/
|
|
9
|
+
export function applyPagination({
|
|
10
|
+
query,
|
|
11
|
+
page,
|
|
12
|
+
limit,
|
|
13
|
+
}: {
|
|
14
|
+
query: import('knex').Knex.QueryBuilder
|
|
15
|
+
page?: number
|
|
16
|
+
limit?: number
|
|
17
|
+
}): import('knex').Knex.QueryBuilder<any, any>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes paginated query.
|
|
3
|
+
*
|
|
4
|
+
* @async
|
|
5
|
+
*/
|
|
6
|
+
export function sqlPaginate({
|
|
7
|
+
mapRow,
|
|
8
|
+
orderBy,
|
|
9
|
+
page,
|
|
10
|
+
baseQuery,
|
|
11
|
+
limit,
|
|
12
|
+
filter,
|
|
13
|
+
snakeCase,
|
|
14
|
+
}: {
|
|
15
|
+
mapRow: any
|
|
16
|
+
orderBy: any
|
|
17
|
+
page?: number
|
|
18
|
+
baseQuery: any
|
|
19
|
+
limit?: number
|
|
20
|
+
filter?: {}
|
|
21
|
+
snakeCase?: boolean
|
|
22
|
+
}): Promise<{
|
|
23
|
+
totalCount: number
|
|
24
|
+
totalPages: number
|
|
25
|
+
currentPage: number
|
|
26
|
+
hasPrevious: boolean
|
|
27
|
+
hasNext: boolean
|
|
28
|
+
list: any
|
|
29
|
+
}>
|