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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.71",
3
+ "version": "1.3.73",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -1,19 +1,45 @@
1
1
  import { getTableNameFromQuery } from '../core/get-table-name.js'
2
2
 
3
3
  /**
4
- * Applies ORDER BY clause.
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 {{ column: string, direction?: 'asc'|'desc' }} params.orderBy
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?.column) {
23
+ if (!orderBy) {
12
24
  return query
13
25
  }
14
26
 
15
27
  const tableName = getTableNameFromQuery(query)
16
- const column = tableName ? `${tableName}.${orderBy.column}` : orderBy.column
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
- return query.orderBy(column, orderBy.direction || 'asc')
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
- const missingTables = []
45
+ try {
46
+ const missingTables = []
41
47
 
42
- for (const table of tables) {
43
- const exists = await db.schema.hasTable(table)
44
- if (!exists) {
45
- missingTables.push(table)
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
- if (missingTables.length > 0) {
50
- throw new Error(
51
- `Missing the following tables: ${missingTables.join(', ')}. Did you run migrations?`,
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
- if (tables.length) {
56
- log.info(`All required tables are exists: ${tables.join(', ')}`)
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),