core-services-sdk 1.3.65 → 1.3.67

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.65",
3
+ "version": "1.3.67",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -31,6 +31,7 @@
31
31
  "@aws-sdk/credential-provider-node": "^3.862.0",
32
32
  "@sendgrid/mail": "^8.1.5",
33
33
  "amqplib": "^0.10.8",
34
+ "date-fns": "^4.1.0",
34
35
  "dot": "^1.1.3",
35
36
  "fastify": "^5.4.0",
36
37
  "google-libphonenumber": "^3.2.42",
@@ -1,13 +1,71 @@
1
1
  import { applyFilter } from '../filters/apply-filter.js'
2
- import { applyFilterSnakeCase } from '../filters/apply-filter-snake-case.js'
3
2
  import { applyOrderBy } from '../modifiers/apply-order-by.js'
4
3
  import { applyPagination } from '../modifiers/apply-pagination.js'
4
+ import { applyFilterSnakeCase } from '../filters/apply-filter-snake-case.js'
5
5
  import { normalizeNumberOrDefault } from '../../core/normalize-premitives-types-or-default.js'
6
6
 
7
7
  /**
8
- * Executes paginated query.
8
+ * Executes a paginated SQL query using a Knex query builder.
9
+ *
10
+ * This helper clones the provided base query twice:
11
+ * one query is used to fetch the paginated list of rows,
12
+ * and the second query is used to calculate the total count
13
+ * of records matching the same filters.
14
+ *
15
+ * Filters are applied consistently to both queries.
16
+ * Ordering and pagination are applied only to the list query.
17
+ *
18
+ * The function supports both camelCase and snake_case filters,
19
+ * controlled by the `snakeCase` flag.
9
20
  *
10
21
  * @async
22
+ * @param {Object} params
23
+ * @param {import('knex').Knex.QueryBuilder} params.baseQuery
24
+ * A prepared Knex query builder representing the base query.
25
+ * It should not include pagination or ordering.
26
+ *
27
+ * @param {Object} [params.filter={}]
28
+ * An object describing filter conditions to apply.
29
+ * The structure is expected to be compatible with `applyFilter`
30
+ * or `applyFilterSnakeCase`.
31
+ *
32
+ * @param {boolean} [params.snakeCase=true]
33
+ * When true, applies filters assuming database columns
34
+ * are in snake_case. When false, camelCase is assumed.
35
+ *
36
+ * @param {Object|Array} [params.orderBy]
37
+ * Ordering definition passed directly to `applyOrderBy`.
38
+ * Can be a single order definition or an array of them.
39
+ *
40
+ * @param {number} [params.page=1]
41
+ * The current page number. Pages are 1-based.
42
+ *
43
+ * @param {number} [params.limit=10]
44
+ * The maximum number of rows per page.
45
+ *
46
+ * @param {Function} [params.mapRow]
47
+ * Optional mapping function applied to each returned row.
48
+ * Useful for transforming DB rows into domain entities.
49
+ *
50
+ * @returns {Promise<Object>} Pagination result object
51
+ * @returns {number} return.page
52
+ * The current page number.
53
+ *
54
+ * @returns {number} return.pages
55
+ * Total number of available pages.
56
+ *
57
+ * @returns {number} return.totalCount
58
+ * Total number of records matching the filters.
59
+ *
60
+ * @returns {boolean} return.hasPrevious
61
+ * Indicates whether a previous page exists.
62
+ *
63
+ * @returns {boolean} return.hasNext
64
+ * Indicates whether a next page exists.
65
+ *
66
+ * @returns {Array<Object>} return.list
67
+ * The list of records for the current page.
68
+ * Rows are mapped using `mapRow` if provided.
11
69
  */
12
70
  export async function sqlPaginate({
13
71
  mapRow,
@@ -35,14 +93,14 @@ export async function sqlPaginate({
35
93
  ])
36
94
 
37
95
  const totalCount = normalizeNumberOrDefault(countResult?.count || 0)
38
- const totalPages = Math.ceil(totalCount / limit)
96
+ const pages = Math.ceil(totalCount / limit)
39
97
 
40
98
  return {
99
+ page,
100
+ pages,
41
101
  totalCount,
42
- totalPages,
43
- currentPage: page,
44
102
  hasPrevious: page > 1,
45
- hasNext: page < totalPages,
103
+ hasNext: page < pages,
46
104
  list: mapRow ? rows.map(mapRow) : rows,
47
105
  }
48
106
  }
@@ -12,10 +12,10 @@ import { applyFilterSnakeCase } from '../../src/postgresql/filters/apply-filter-
12
12
 
13
13
  const PG_OPTIONS = {
14
14
  port: 5444,
15
- containerName: 'postgres-apply-filter-test-5444',
15
+ db: 'testdb',
16
16
  user: 'testuser',
17
17
  pass: 'testpass',
18
- db: 'testdb',
18
+ containerName: 'postgres-apply-filter-test-5444',
19
19
  }
20
20
 
21
21
  const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
@@ -3,8 +3,8 @@ import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
3
3
  import knex from 'knex'
4
4
 
5
5
  import {
6
- startPostgres,
7
6
  stopPostgres,
7
+ startPostgres,
8
8
  buildPostgresUri,
9
9
  } from '../../src/postgresql/start-stop-postgres-docker.js'
10
10
 
@@ -12,10 +12,10 @@ import { applyFilter } from '../../src/postgresql/filters/apply-filter.js'
12
12
 
13
13
  const PG_OPTIONS = {
14
14
  port: 5443,
15
- containerName: 'postgres-apply-filter-test',
15
+ db: 'testdb',
16
16
  user: 'testuser',
17
17
  pass: 'testpass',
18
- db: 'testdb',
18
+ containerName: 'postgres-apply-filter-test',
19
19
  }
20
20
 
21
21
  const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
@@ -9,13 +9,14 @@ import {
9
9
  } from '../../src/postgresql/start-stop-postgres-docker.js'
10
10
 
11
11
  import { sqlPaginate } from '../../src/postgresql/pagination/paginate.js'
12
+ import { sub } from 'date-fns'
12
13
 
13
14
  const PG_OPTIONS = {
14
15
  port: 5442,
15
- containerName: 'postgres-paginate-test-5442',
16
+ db: 'testdb',
16
17
  user: 'testuser',
17
18
  pass: 'testpass',
18
- db: 'testdb',
19
+ containerName: 'postgres-paginate-test-5442',
19
20
  }
20
21
 
21
22
  const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
@@ -34,6 +35,7 @@ beforeAll(async () => {
34
35
  table.uuid('id').primary()
35
36
  table.string('name').notNullable()
36
37
  table.string('type').notNullable()
38
+ table.bigInteger('age').notNullable()
37
39
  table.timestamp('created_at').notNullable()
38
40
  })
39
41
  })
@@ -47,41 +49,60 @@ afterAll(async () => {
47
49
 
48
50
  beforeEach(async () => {
49
51
  await db('tenants').truncate()
50
-
51
- await db('tenants').insert([
52
+ const records = [
52
53
  {
53
54
  id: '00000000-0000-0000-0000-000000000001',
54
55
  name: 'Tenant A',
55
56
  type: 'business',
56
- created_at: new Date('2024-01-01'),
57
+ age: 2,
58
+ created_at: new Date('2025-01-01'),
57
59
  },
58
60
  {
59
61
  id: '00000000-0000-0000-0000-000000000002',
60
62
  name: 'Tenant B',
63
+ age: 3,
61
64
  type: 'business',
62
65
  created_at: new Date('2024-01-02'),
63
66
  },
64
67
  {
65
68
  id: '00000000-0000-0000-0000-000000000003',
66
69
  name: 'Tenant C',
70
+ age: 1,
71
+ type: 'cpa',
72
+ created_at: new Date('2023-01-03'),
73
+ },
74
+
75
+ {
76
+ id: '00000000-0000-0000-0000-000000000004',
77
+ name: 'Tenant D',
78
+ age: 7,
79
+ type: 'cpa',
80
+ created_at: new Date('2022-01-03'),
81
+ },
82
+
83
+ {
84
+ id: '00000000-0000-0000-0000-000000000005',
85
+ name: 'Tenant E',
86
+ age: 0,
67
87
  type: 'cpa',
68
- created_at: new Date('2024-01-03'),
88
+ created_at: new Date('2021-01-03'),
69
89
  },
70
- ])
90
+ ]
91
+ await db('tenants').insert(records)
71
92
  })
72
93
 
73
94
  describe('paginate integration', () => {
74
95
  it('returns first page without ordering guarantees', async () => {
75
96
  const result = await sqlPaginate({
76
97
  baseQuery: db('tenants'),
77
- page: 1,
98
+ page: 2,
78
99
  limit: 2,
79
100
  })
80
101
 
81
- expect(result.totalCount).toBe(3)
82
- expect(result.totalPages).toBe(2)
83
- expect(result.currentPage).toBe(1)
84
- expect(result.hasPrevious).toBe(false)
102
+ expect(result.totalCount).toBe(5)
103
+ expect(result.pages).toBe(3)
104
+ expect(result.page).toBe(2)
105
+ expect(result.hasPrevious).toBe(true)
85
106
  expect(result.hasNext).toBe(true)
86
107
  expect(result.list).toHaveLength(2)
87
108
  })
@@ -93,19 +114,23 @@ describe('paginate integration', () => {
93
114
  limit: 2,
94
115
  })
95
116
 
96
- expect(result.totalCount).toBe(3)
97
- expect(result.totalPages).toBe(2)
98
- expect(result.currentPage).toBe(2)
117
+ expect(result.totalCount).toBe(5)
118
+ expect(result.pages).toBe(3)
119
+ expect(result.page).toBe(2)
99
120
  expect(result.hasPrevious).toBe(true)
100
- expect(result.hasNext).toBe(false)
101
- expect(result.list).toHaveLength(1)
121
+ expect(result.hasNext).toBe(true)
122
+ expect(result.list).toHaveLength(2)
102
123
  })
103
124
 
104
125
  it('applies filters correctly', async () => {
126
+ const minDate = sub(new Date(), { years: 2 })
105
127
  const result = await sqlPaginate({
106
128
  baseQuery: db('tenants'),
107
- filter: { type: 'business' },
129
+ filter: {
130
+ createdAt: { lte: new Date(), gte: minDate },
131
+ },
108
132
  limit: 10,
133
+ page: 1,
109
134
  })
110
135
 
111
136
  expect(result.totalCount).toBe(2)
@@ -118,7 +143,7 @@ describe('paginate integration', () => {
118
143
  const result = await sqlPaginate({
119
144
  baseQuery: db('tenants'),
120
145
  orderBy: {
121
- column: 'created_at',
146
+ column: 'name',
122
147
  direction: 'asc',
123
148
  },
124
149
  })
@@ -127,6 +152,8 @@ describe('paginate integration', () => {
127
152
  'Tenant A',
128
153
  'Tenant B',
129
154
  'Tenant C',
155
+ 'Tenant D',
156
+ 'Tenant E',
130
157
  ])
131
158
  })
132
159
 
@@ -150,7 +177,7 @@ describe('paginate integration', () => {
150
177
  })
151
178
 
152
179
  expect(result.totalCount).toBe(0)
153
- expect(result.totalPages).toBe(0)
180
+ expect(result.pages).toBe(0)
154
181
  expect(result.list).toEqual([])
155
182
  expect(result.hasNext).toBe(false)
156
183
  expect(result.hasPrevious).toBe(false)
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import { applyFilter } from '../filters/apply-filter.js'
2
3
  import { applyFilterSnakeCase } from '../filters/apply-filter-snake-case.js'
3
4
  import { applyOrderBy } from '../modifiers/apply-order-by.js'
@@ -35,14 +36,14 @@ export async function sqlPaginate({
35
36
  ])
36
37
 
37
38
  const totalCount = normalizeNumberOrDefault(countResult?.count || 0)
38
- const totalPages = Math.ceil(totalCount / limit)
39
+ const pages = Math.ceil(totalCount / limit)
39
40
 
40
41
  return {
41
42
  totalCount,
42
- totalPages,
43
+ pages,
43
44
  currentPage: page,
44
45
  hasPrevious: page > 1,
45
- hasNext: page < totalPages,
46
+ hasNext: page < pages,
46
47
  list: mapRow ? rows.map(mapRow) : rows,
47
48
  }
48
49
  }
@@ -11,10 +11,10 @@ import { validateSchema } from '../../src/postgresql/validate-schema.js'
11
11
 
12
12
  const PG_OPTIONS = {
13
13
  port: 5431,
14
- containerName: 'postgres-validate-schema-test-5431',
14
+ db: 'testdb',
15
15
  user: 'testuser',
16
16
  pass: 'testpass',
17
- db: 'testdb',
17
+ containerName: 'postgres-validate-schema-test-5431',
18
18
  }
19
19
 
20
20
  const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
@@ -1,7 +1,65 @@
1
1
  /**
2
- * Executes paginated query.
2
+ * Executes a paginated SQL query using a Knex query builder.
3
+ *
4
+ * This helper clones the provided base query twice:
5
+ * one query is used to fetch the paginated list of rows,
6
+ * and the second query is used to calculate the total count
7
+ * of records matching the same filters.
8
+ *
9
+ * Filters are applied consistently to both queries.
10
+ * Ordering and pagination are applied only to the list query.
11
+ *
12
+ * The function supports both camelCase and snake_case filters,
13
+ * controlled by the `snakeCase` flag.
3
14
  *
4
15
  * @async
16
+ * @param {Object} params
17
+ * @param {import('knex').Knex.QueryBuilder} params.baseQuery
18
+ * A prepared Knex query builder representing the base query.
19
+ * It should not include pagination or ordering.
20
+ *
21
+ * @param {Object} [params.filter={}]
22
+ * An object describing filter conditions to apply.
23
+ * The structure is expected to be compatible with `applyFilter`
24
+ * or `applyFilterSnakeCase`.
25
+ *
26
+ * @param {boolean} [params.snakeCase=true]
27
+ * When true, applies filters assuming database columns
28
+ * are in snake_case. When false, camelCase is assumed.
29
+ *
30
+ * @param {Object|Array} [params.orderBy]
31
+ * Ordering definition passed directly to `applyOrderBy`.
32
+ * Can be a single order definition or an array of them.
33
+ *
34
+ * @param {number} [params.page=1]
35
+ * The current page number. Pages are 1-based.
36
+ *
37
+ * @param {number} [params.limit=10]
38
+ * The maximum number of rows per page.
39
+ *
40
+ * @param {Function} [params.mapRow]
41
+ * Optional mapping function applied to each returned row.
42
+ * Useful for transforming DB rows into domain entities.
43
+ *
44
+ * @returns {Promise<Object>} Pagination result object
45
+ * @returns {number} return.page
46
+ * The current page number.
47
+ *
48
+ * @returns {number} return.pages
49
+ * Total number of available pages.
50
+ *
51
+ * @returns {number} return.totalCount
52
+ * Total number of records matching the filters.
53
+ *
54
+ * @returns {boolean} return.hasPrevious
55
+ * Indicates whether a previous page exists.
56
+ *
57
+ * @returns {boolean} return.hasNext
58
+ * Indicates whether a next page exists.
59
+ *
60
+ * @returns {Array<Object>} return.list
61
+ * The list of records for the current page.
62
+ * Rows are mapped using `mapRow` if provided.
5
63
  */
6
64
  export function sqlPaginate({
7
65
  mapRow,
@@ -12,18 +70,11 @@ export function sqlPaginate({
12
70
  filter,
13
71
  snakeCase,
14
72
  }: {
15
- mapRow: any
16
- orderBy: any
73
+ baseQuery: import('knex').Knex.QueryBuilder
74
+ filter?: any
75
+ snakeCase?: boolean
76
+ orderBy?: any | any[]
17
77
  page?: number
18
- baseQuery: any
19
78
  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
- }>
79
+ mapRow?: Function
80
+ }): Promise<any>