core-services-sdk 1.3.63 → 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/core/normalize-phone-number.js +144 -25
- 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 +45 -0
- package/tests/postgresql/apply-filter-snake-case.integration.test.js +220 -0
- package/tests/postgresql/apply-filter.integration.test.js +34 -353
- 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 +10 -18
- package/tests/postgresql/pagination/paginate.js +48 -0
- package/tests/postgresql/validate-schema.integration.test.js +9 -5
- package/types/core/normalize-phone-number.d.ts +97 -27
- package/types/postgresql/apply-filter.d.ts +131 -20
- 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 -275
- package/src/postgresql/paginate.js +0 -61
|
@@ -8,11 +8,11 @@ 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
|
-
port:
|
|
15
|
-
containerName: 'postgres-paginate-test',
|
|
14
|
+
port: 5442,
|
|
15
|
+
containerName: 'postgres-paginate-test-5442',
|
|
16
16
|
user: 'testuser',
|
|
17
17
|
pass: 'testpass',
|
|
18
18
|
db: 'testdb',
|
|
@@ -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,12 +5,13 @@ 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
|
|
|
11
12
|
const PG_OPTIONS = {
|
|
12
|
-
port:
|
|
13
|
-
containerName: 'postgres-validate-schema-test',
|
|
13
|
+
port: 5431,
|
|
14
|
+
containerName: 'postgres-validate-schema-test-5431',
|
|
14
15
|
user: 'testuser',
|
|
15
16
|
pass: 'testpass',
|
|
16
17
|
db: 'testdb',
|
|
@@ -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
|
|
|
@@ -1,25 +1,82 @@
|
|
|
1
|
-
/** Resolve libphonenumber regardless of interop shape */
|
|
2
|
-
export function getLib(): any
|
|
3
|
-
export function phoneUtil(): any
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
8
|
-
* @
|
|
2
|
+
* @typedef {Object} NormalizedPhone
|
|
3
|
+
* @property {string} e164
|
|
4
|
+
* @property {number} type
|
|
5
|
+
* @property {string} national
|
|
6
|
+
* @property {number} countryCode
|
|
7
|
+
* @property {string} nationalClean
|
|
8
|
+
* @property {string} international
|
|
9
|
+
* @property {string} countryCodeE164
|
|
10
|
+
* @property {string} internationalClean
|
|
11
|
+
* @property {string | undefined} regionCode
|
|
9
12
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* normalize-phone-number.js
|
|
15
|
+
*
|
|
16
|
+
* Utilities for parsing, validating, and normalizing phone numbers
|
|
17
|
+
* using google-libphonenumber.
|
|
18
|
+
*
|
|
19
|
+
* Supports both ESM and CJS interop builds.
|
|
20
|
+
* All normalization outputs are canonical and safe for persistence,
|
|
21
|
+
* comparison, indexing, and login identifiers.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Resolve google-libphonenumber exports regardless of ESM / CJS shape.
|
|
25
|
+
*
|
|
26
|
+
* Some builds expose:
|
|
27
|
+
* - raw.PhoneNumberUtil
|
|
28
|
+
* Others expose:
|
|
29
|
+
* - raw.default.PhoneNumberUtil
|
|
30
|
+
*
|
|
31
|
+
* This helper guarantees a consistent API.
|
|
32
|
+
*
|
|
33
|
+
* @returns {{
|
|
34
|
+
* PhoneNumberUtil: any,
|
|
35
|
+
* PhoneNumberFormat: any
|
|
36
|
+
* }}
|
|
37
|
+
* @throws {Error} If required exports are missing
|
|
38
|
+
*/
|
|
39
|
+
export function getLib(): {
|
|
40
|
+
PhoneNumberUtil: any
|
|
41
|
+
PhoneNumberFormat: any
|
|
16
42
|
}
|
|
17
43
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
44
|
+
* Lazy singleton accessor for PhoneNumberUtil.
|
|
45
|
+
*
|
|
46
|
+
* Ensures:
|
|
47
|
+
* - Single instance per process
|
|
48
|
+
* - No eager initialization cost
|
|
49
|
+
*
|
|
50
|
+
* @returns {any} PhoneNumberUtil instance
|
|
51
|
+
*/
|
|
52
|
+
export function phoneUtil(): any
|
|
53
|
+
/**
|
|
54
|
+
* Normalize and validate an international phone number.
|
|
55
|
+
*
|
|
56
|
+
* Input MUST start with '+'.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} input International phone number (E.164-like)
|
|
59
|
+
* @returns {NormalizedPhone}
|
|
60
|
+
* @throws {Error} If the phone number is invalid
|
|
61
|
+
*/
|
|
62
|
+
export function normalizePhoneOrThrowIntl(input: string): NormalizedPhone
|
|
63
|
+
/**
|
|
64
|
+
* Normalize and validate a national phone number using a region hint.
|
|
65
|
+
*
|
|
66
|
+
* Example:
|
|
67
|
+
* input: "0523444444"
|
|
68
|
+
* defaultRegion: "IL"
|
|
69
|
+
*
|
|
70
|
+
* @param {string} input National phone number
|
|
71
|
+
* @param {string} defaultRegion ISO 3166-1 alpha-2 country code
|
|
72
|
+
* @returns {{
|
|
73
|
+
* e164: string,
|
|
74
|
+
* national: string,
|
|
75
|
+
* international: string,
|
|
76
|
+
* regionCode: string | undefined,
|
|
77
|
+
* type: number
|
|
78
|
+
* }}
|
|
79
|
+
* @throws {Error} If the phone number is invalid
|
|
23
80
|
*/
|
|
24
81
|
export function normalizePhoneOrThrowWithRegion(
|
|
25
82
|
input: string,
|
|
@@ -32,23 +89,36 @@ export function normalizePhoneOrThrowWithRegion(
|
|
|
32
89
|
type: number
|
|
33
90
|
}
|
|
34
91
|
/**
|
|
35
|
-
* Smart normalization
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
92
|
+
* Smart normalization entry point.
|
|
93
|
+
*
|
|
94
|
+
* Behavior:
|
|
95
|
+
* - If input starts with '+', parses as international
|
|
96
|
+
* - Otherwise requires defaultRegion and parses as national
|
|
97
|
+
*
|
|
98
|
+
* This is the recommended function for login, signup, and verification flows.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} input Phone number (international or national)
|
|
101
|
+
* @param {{
|
|
102
|
+
* defaultRegion?: string
|
|
103
|
+
* }} [opts]
|
|
104
|
+
*
|
|
105
|
+
* @returns {NormalizedPhone}
|
|
106
|
+
* @throws {Error} If invalid or defaultRegion is missing
|
|
42
107
|
*/
|
|
43
108
|
export function normalizePhoneOrThrow(
|
|
44
109
|
input: string,
|
|
45
110
|
opts?: {
|
|
46
111
|
defaultRegion?: string
|
|
47
112
|
},
|
|
48
|
-
):
|
|
113
|
+
): NormalizedPhone
|
|
114
|
+
export type NormalizedPhone = {
|
|
49
115
|
e164: string
|
|
116
|
+
type: number
|
|
50
117
|
national: string
|
|
118
|
+
countryCode: number
|
|
119
|
+
nationalClean: string
|
|
51
120
|
international: string
|
|
121
|
+
countryCodeE164: string
|
|
122
|
+
internationalClean: string
|
|
52
123
|
regionCode: string | undefined
|
|
53
|
-
type: number
|
|
54
124
|
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Builds a Knex query with MongoDB-style filter operators.
|
|
3
3
|
* Pure utility function that can be used across repositories.
|
|
4
4
|
*
|
|
5
|
-
* This function
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* This function applies various filter operators to build SQL WHERE clauses. All column names
|
|
6
|
+
* are automatically qualified with the provided table name. Filter keys are used as-is without
|
|
7
|
+
* any case conversion. If you need automatic camelCase to snake_case conversion, use
|
|
8
|
+
* {@link applyFilterSnakeCase} instead.
|
|
8
9
|
*
|
|
9
10
|
* **Supported Operators:**
|
|
10
11
|
* - `eq` - Equality (default for simple values)
|
|
@@ -21,15 +22,19 @@
|
|
|
21
22
|
* - `isNotNull` - Check if field is NOT NULL
|
|
22
23
|
*
|
|
23
24
|
* **Key Features:**
|
|
24
|
-
* -
|
|
25
|
+
* - Filter keys are used as-is (no automatic case conversion)
|
|
25
26
|
* - Qualified column names (table.column format)
|
|
26
27
|
* - Multiple operators can be applied to the same field
|
|
27
28
|
* - Unknown operators are silently ignored
|
|
28
29
|
* - Arrays are automatically converted to IN clauses
|
|
29
30
|
*
|
|
31
|
+
* **Note:** This function does NOT convert filter keys. If your database uses snake_case columns
|
|
32
|
+
* but your filter keys are in camelCase, use {@link applyFilterSnakeCase} instead, or ensure
|
|
33
|
+
* your filter keys already match your database column names.
|
|
34
|
+
*
|
|
30
35
|
* @param {Object} params - Function parameters
|
|
31
36
|
* @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance to apply filters to
|
|
32
|
-
* @param {Object<string, *>} params.filter - Filter object with
|
|
37
|
+
* @param {Object<string, *>} params.filter - Filter object with keys matching database column names (used as-is)
|
|
33
38
|
* Filter values can be:
|
|
34
39
|
* - Simple values (string, number, boolean) → treated as equality
|
|
35
40
|
* - Arrays → treated as IN operator
|
|
@@ -89,9 +94,10 @@
|
|
|
89
94
|
* })
|
|
90
95
|
*
|
|
91
96
|
* @example
|
|
92
|
-
* //
|
|
93
|
-
* applyFilter({ query, filter: {
|
|
97
|
+
* // Filter keys are used as-is - ensure they match your database column names
|
|
98
|
+
* applyFilter({ query, filter: { deleted_at: { isNull: true } }, tableName: 'assets' })
|
|
94
99
|
* // SQL: WHERE assets.deleted_at IS NULL
|
|
100
|
+
* // Note: If you need camelCase conversion, use applyFilterSnakeCase instead
|
|
95
101
|
*/
|
|
96
102
|
export function applyFilter({
|
|
97
103
|
query,
|
|
@@ -104,17 +110,122 @@ export function applyFilter({
|
|
|
104
110
|
}
|
|
105
111
|
tableName: string
|
|
106
112
|
}): import('knex').Knex.QueryBuilder
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Applies MongoDB-style filter operators to a Knex query with automatic camelCase to snake_case conversion.
|
|
115
|
+
*
|
|
116
|
+
* This function is a convenience wrapper around {@link applyFilter} that automatically converts
|
|
117
|
+
* all filter keys from camelCase to snake_case before applying the filters. This is useful when
|
|
118
|
+
* your application code uses camelCase naming conventions but your database columns use snake_case.
|
|
119
|
+
*
|
|
120
|
+
* The function first converts all filter object keys using `toSnakeCase`, then applies the filters
|
|
121
|
+
* using the standard `applyFilter` function. This ensures that filter keys like `userId` are
|
|
122
|
+
* converted to `user_id` before being used in SQL queries.
|
|
123
|
+
*
|
|
124
|
+
* **Key Features:**
|
|
125
|
+
* - Automatic camelCase to snake_case key conversion
|
|
126
|
+
* - Same operator support as `applyFilter`
|
|
127
|
+
* - Qualified column names (table.column format)
|
|
128
|
+
* - Multiple operators can be applied to the same field
|
|
129
|
+
* - Arrays are automatically converted to IN clauses
|
|
130
|
+
*
|
|
131
|
+
* **Supported Operators:**
|
|
132
|
+
* Same as {@link applyFilter}:
|
|
133
|
+
* - `eq` - Equality (default for simple values)
|
|
134
|
+
* - `ne` / `neq` - Not equal
|
|
135
|
+
* - `in` - Array membership (or pass array directly)
|
|
136
|
+
* - `nin` - Not in array
|
|
137
|
+
* - `gt` - Greater than
|
|
138
|
+
* - `gte` - Greater than or equal
|
|
139
|
+
* - `lt` - Less than
|
|
140
|
+
* - `lte` - Less than or equal
|
|
141
|
+
* - `like` - Case-sensitive pattern matching (SQL LIKE)
|
|
142
|
+
* - `ilike` - Case-insensitive pattern matching (PostgreSQL ILIKE)
|
|
143
|
+
* - `isNull` - Check if field is NULL
|
|
144
|
+
* - `isNotNull` - Check if field is NOT NULL
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} params - Function parameters
|
|
147
|
+
* @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance to apply filters to
|
|
148
|
+
* @param {Object<string, *>} params.filter - Filter object with camelCase keys (will be converted to snake_case)
|
|
149
|
+
* Filter values can be:
|
|
150
|
+
* - Simple values (string, number, boolean) → treated as equality
|
|
151
|
+
* - Arrays → treated as IN operator
|
|
152
|
+
* - Objects with operator keys → apply specific operators
|
|
153
|
+
* @param {string} params.tableName - Table name used to qualify column names (e.g., "assets" → "assets.column_name")
|
|
154
|
+
* @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied WHERE clauses (using snake_case column names)
|
|
155
|
+
*
|
|
156
|
+
* @throws {TypeError} If query is not a valid Knex QueryBuilder instance
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* // Simple equality with camelCase key - converts to WHERE assets.user_id = 1
|
|
160
|
+
* const query = db('assets').select('*')
|
|
161
|
+
* applyFilterSnakeCase({ query, filter: { userId: 1 }, tableName: 'assets' })
|
|
162
|
+
* // SQL: WHERE assets.user_id = 1
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* // Not equal with camelCase key - converts to WHERE assets.status != 'deleted'
|
|
166
|
+
* applyFilterSnakeCase({
|
|
167
|
+
* query,
|
|
168
|
+
* filter: { status: { ne: 'deleted' } },
|
|
169
|
+
* tableName: 'assets'
|
|
170
|
+
* })
|
|
171
|
+
* // SQL: WHERE assets.status != 'deleted'
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* // Array/IN operator with camelCase key - converts to WHERE assets.status IN ('active', 'pending')
|
|
175
|
+
* applyFilterSnakeCase({
|
|
176
|
+
* query,
|
|
177
|
+
* filter: { status: ['active', 'pending'] },
|
|
178
|
+
* tableName: 'assets'
|
|
179
|
+
* })
|
|
180
|
+
* // SQL: WHERE assets.status IN ('active', 'pending')
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* // Range operators with camelCase keys - converts to WHERE assets.price >= 100 AND assets.price <= 200
|
|
184
|
+
* applyFilterSnakeCase({
|
|
185
|
+
* query,
|
|
186
|
+
* filter: { price: { gte: 100, lte: 200 } },
|
|
187
|
+
* tableName: 'assets'
|
|
188
|
+
* })
|
|
189
|
+
* // SQL: WHERE assets.price >= 100 AND assets.price <= 200
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* // Null checks with camelCase key - converts to WHERE assets.deleted_at IS NULL
|
|
193
|
+
* applyFilterSnakeCase({
|
|
194
|
+
* query,
|
|
195
|
+
* filter: { deletedAt: { isNull: true } },
|
|
196
|
+
* tableName: 'assets'
|
|
197
|
+
* })
|
|
198
|
+
* // SQL: WHERE assets.deleted_at IS NULL
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* // Multiple filters with camelCase keys
|
|
202
|
+
* applyFilterSnakeCase({
|
|
203
|
+
* query,
|
|
204
|
+
* filter: {
|
|
205
|
+
* userId: 123, // Converts to user_id
|
|
206
|
+
* createdAt: { gte: '2024-01-01' }, // Converts to created_at
|
|
207
|
+
* status: 'active'
|
|
208
|
+
* },
|
|
209
|
+
* tableName: 'assets'
|
|
210
|
+
* })
|
|
211
|
+
* // SQL: WHERE assets.user_id = 123 AND assets.created_at >= '2024-01-01' AND assets.status = 'active'
|
|
212
|
+
*
|
|
213
|
+
* @see {@link applyFilter} For the base function without key conversion
|
|
214
|
+
* @see {@link toSnakeCase} For details on the key conversion process
|
|
215
|
+
*/
|
|
216
|
+
export function applyFilterSnakeCase({
|
|
217
|
+
query,
|
|
218
|
+
filter,
|
|
219
|
+
tableName,
|
|
220
|
+
}: {
|
|
111
221
|
query: import('knex').Knex.QueryBuilder
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
222
|
+
filter: {
|
|
223
|
+
[x: string]: any
|
|
224
|
+
}
|
|
225
|
+
tableName: string
|
|
226
|
+
}): import('knex').Knex.QueryBuilder
|
|
227
|
+
/**
|
|
228
|
+
* Type definition for filter operator functions.
|
|
229
|
+
* Each operator function applies a WHERE condition to a Knex query builder.
|
|
230
|
+
*/
|
|
231
|
+
export type OperatorFunction = Function
|
|
@@ -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
|
+
}>
|