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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/src/postgresql/core/get-table-name.js +10 -0
  3. package/src/postgresql/filters/apply-filter-object.js +19 -0
  4. package/src/postgresql/filters/apply-filter-snake-case.js +16 -0
  5. package/src/postgresql/filters/apply-filter.js +33 -0
  6. package/src/postgresql/filters/operators.js +32 -0
  7. package/src/postgresql/index.js +5 -2
  8. package/src/postgresql/modifiers/apply-order-by.js +19 -0
  9. package/src/postgresql/modifiers/apply-pagination.js +12 -0
  10. package/src/postgresql/pagination/paginate.js +48 -0
  11. package/tests/core/normalize-phone-number.unit.test.js +1 -1
  12. package/tests/postgresql/apply-filter-snake-case.integration.test.js +25 -354
  13. package/tests/postgresql/apply-filter.integration.test.js +28 -78
  14. package/tests/postgresql/core/get-table-name.unit.test.js +20 -0
  15. package/tests/postgresql/filters/apply-filter-object.test.js +23 -0
  16. package/tests/postgresql/filters/operators.unit.test.js +23 -0
  17. package/tests/postgresql/modifiers/apply-order-by.test.js +80 -0
  18. package/tests/postgresql/modifiers/apply-pagination.unit.test.js +18 -0
  19. package/tests/postgresql/paginate.integration.test.js +8 -16
  20. package/tests/postgresql/pagination/paginate.js +48 -0
  21. package/tests/postgresql/validate-schema.integration.test.js +7 -3
  22. package/types/postgresql/core/get-table-name.d.ts +10 -0
  23. package/types/postgresql/filters/apply-filter-object.d.ts +15 -0
  24. package/types/postgresql/filters/apply-filter-snake-case.d.ts +14 -0
  25. package/types/postgresql/filters/apply-filter.d.ts +15 -0
  26. package/types/postgresql/filters/operators.d.ts +14 -0
  27. package/types/postgresql/index.d.ts +5 -2
  28. package/types/postgresql/modifiers/apply-order-by.d.ts +17 -0
  29. package/types/postgresql/modifiers/apply-pagination.d.ts +17 -0
  30. package/types/postgresql/pagination/paginate.d.ts +29 -0
  31. package/src/postgresql/apply-filter.js +0 -401
  32. 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 without ordering guarantees', async () => {
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({ connection: DATABASE_URI, tables: ['files'] }),
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
+ }>