core-services-sdk 1.3.72 → 1.3.74
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 +3 -2
- package/src/env/env-validation.js +309 -0
- package/src/postgresql/validate-schema.js +23 -14
- package/tests/env/build-env-report.test.js +132 -0
- package/tests/env/create-zod-schema.test.js +175 -0
- package/tests/env/env-demo.js +154 -0
- package/tests/env/validate-env.test.js +108 -0
- package/types/env/env-validation.d.ts +157 -0
- package/types/postgresql/modifiers/apply-order-by.d.ts +19 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.74",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"pino": "^9.7.0",
|
|
46
46
|
"ulid": "^3.0.1",
|
|
47
47
|
"uuid": "^11.1.0",
|
|
48
|
-
"xml2js": "^0.6.2"
|
|
48
|
+
"xml2js": "^0.6.2",
|
|
49
|
+
"zod": "^4.3.6"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@vitest/coverage-v8": "^3.2.4",
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { mask as defaultMask } from '../util/mask-sensitive.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a single field definition into a Zod schema.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} def
|
|
8
|
+
* @param {'string'|'number'|'boolean'} def.type
|
|
9
|
+
*
|
|
10
|
+
* @param {boolean} [def.required]
|
|
11
|
+
* Whether the value is required. Defaults to false.
|
|
12
|
+
*
|
|
13
|
+
* @param {*} [def.default]
|
|
14
|
+
* Default value if the variable is missing.
|
|
15
|
+
*
|
|
16
|
+
* @param {boolean} [def.secret]
|
|
17
|
+
* Marks the variable as secret (used only for masking/logging).
|
|
18
|
+
*
|
|
19
|
+
* // string options
|
|
20
|
+
* @param {number} [def.minLength]
|
|
21
|
+
* @param {number} [def.maxLength]
|
|
22
|
+
* @param {string} [def.pattern]
|
|
23
|
+
* @param {string[]} [def.enum]
|
|
24
|
+
* @param {'email'|'url'} [def.format]
|
|
25
|
+
*
|
|
26
|
+
* // number options
|
|
27
|
+
* @param {number} [def.min]
|
|
28
|
+
* @param {number} [def.max]
|
|
29
|
+
* @param {boolean} [def.int]
|
|
30
|
+
* @param {boolean} [def.positive]
|
|
31
|
+
* @param {boolean} [def.negative]
|
|
32
|
+
*
|
|
33
|
+
* @returns {import('zod').ZodTypeAny}
|
|
34
|
+
*/
|
|
35
|
+
export function defToZod(def) {
|
|
36
|
+
let schema
|
|
37
|
+
|
|
38
|
+
switch (def.type) {
|
|
39
|
+
case 'string': {
|
|
40
|
+
schema = z.string()
|
|
41
|
+
|
|
42
|
+
if (def.minLength !== undefined) {
|
|
43
|
+
schema = schema.min(def.minLength)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (def.maxLength !== undefined) {
|
|
47
|
+
schema = schema.max(def.maxLength)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (def.pattern) {
|
|
51
|
+
schema = schema.regex(new RegExp(def.pattern))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (def.enum) {
|
|
55
|
+
schema = z.enum(def.enum)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (def.format === 'email') {
|
|
59
|
+
schema = schema.email()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (def.format === 'url') {
|
|
63
|
+
schema = schema.url()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'number': {
|
|
70
|
+
schema = z.coerce.number()
|
|
71
|
+
|
|
72
|
+
if (def.int) {
|
|
73
|
+
schema = schema.int()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (def.min !== undefined) {
|
|
77
|
+
schema = schema.min(def.min)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (def.max !== undefined) {
|
|
81
|
+
schema = schema.max(def.max)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (def.positive) {
|
|
85
|
+
schema = schema.positive()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (def.negative) {
|
|
89
|
+
schema = schema.negative()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'boolean': {
|
|
96
|
+
schema = z.preprocess((val) => {
|
|
97
|
+
if (val === undefined) {
|
|
98
|
+
return val
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof val === 'boolean') {
|
|
102
|
+
return val
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof val === 'string') {
|
|
106
|
+
const normalized = val.toLowerCase().trim()
|
|
107
|
+
|
|
108
|
+
if (normalized === 'true') {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
if (normalized === 'false') {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
if (normalized === '1') {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
if (normalized === '0') {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return val
|
|
123
|
+
}, z.boolean())
|
|
124
|
+
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
default: {
|
|
129
|
+
throw new Error(`Unsupported type: ${def.type}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (def.default !== undefined) {
|
|
134
|
+
schema = schema.default(def.default)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!def.required) {
|
|
138
|
+
schema = schema.optional()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return schema
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds a Zod object schema from a JSON definition map.
|
|
146
|
+
*
|
|
147
|
+
* @param {Record<string, Object>} definition
|
|
148
|
+
* Map of environment variable names to field definitions.
|
|
149
|
+
*
|
|
150
|
+
* @returns {import('zod').ZodObject<any>}
|
|
151
|
+
*/
|
|
152
|
+
export function createZodSchema(definition) {
|
|
153
|
+
const shape = Object.entries(definition).reduce((shape, [key, def]) => {
|
|
154
|
+
return {
|
|
155
|
+
...shape,
|
|
156
|
+
[key]: defToZod(def),
|
|
157
|
+
}
|
|
158
|
+
}, {})
|
|
159
|
+
|
|
160
|
+
return z.object(shape)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validates values using a JSON definition and Zod.
|
|
165
|
+
*
|
|
166
|
+
* @param {Record<string, Object>} definition
|
|
167
|
+
* @param {Record<string, any>} values
|
|
168
|
+
*
|
|
169
|
+
* @returns {{
|
|
170
|
+
* success: true,
|
|
171
|
+
* data: Record<string, any>
|
|
172
|
+
* } | {
|
|
173
|
+
* success: false,
|
|
174
|
+
* summary: Record<string, string[]>
|
|
175
|
+
* }}
|
|
176
|
+
*/
|
|
177
|
+
export function validateEnv(definition, values) {
|
|
178
|
+
const schema = createZodSchema(definition)
|
|
179
|
+
const result = schema.safeParse(values)
|
|
180
|
+
|
|
181
|
+
if (result.success) {
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
data: result.data,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const summary = result.error.issues.reduce((acc, issue) => {
|
|
189
|
+
const key = issue.path[0] || 'root'
|
|
190
|
+
acc[key] = acc[key] || []
|
|
191
|
+
acc[key].push(issue.message)
|
|
192
|
+
return acc
|
|
193
|
+
}, {})
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
summary,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Builds a structured environment validation report.
|
|
203
|
+
*
|
|
204
|
+
* @param {Record<string, Object>} definition
|
|
205
|
+
* @param {Record<string, any>} values
|
|
206
|
+
* Raw input values (e.g. process.env)
|
|
207
|
+
*
|
|
208
|
+
* @param {{
|
|
209
|
+
* success: boolean,
|
|
210
|
+
* data?: Record<string, any>,
|
|
211
|
+
* summary?: Record<string, string[]>
|
|
212
|
+
* }} validationResult
|
|
213
|
+
*
|
|
214
|
+
* @param {(value: any) => string} mask
|
|
215
|
+
*
|
|
216
|
+
* @returns {{
|
|
217
|
+
* success: boolean,
|
|
218
|
+
* params: Array<{
|
|
219
|
+
* key: string,
|
|
220
|
+
* value: any,
|
|
221
|
+
* displayValue: string,
|
|
222
|
+
* secret: boolean,
|
|
223
|
+
* valid: boolean,
|
|
224
|
+
* errors?: string[]
|
|
225
|
+
* }>
|
|
226
|
+
* }}
|
|
227
|
+
*/
|
|
228
|
+
export function buildEnvReport(definition, values, validationResult, mask) {
|
|
229
|
+
const maskValue = typeof mask === 'function' ? mask : defaultMask
|
|
230
|
+
|
|
231
|
+
const params = Object.keys(definition).map((key) => {
|
|
232
|
+
const def = definition[key]
|
|
233
|
+
const isSecret = Boolean(def.secret)
|
|
234
|
+
|
|
235
|
+
// value precedence:
|
|
236
|
+
// 1. validated & parsed value
|
|
237
|
+
// 2. raw input value
|
|
238
|
+
const rawValue = values[key]
|
|
239
|
+
const value = validationResult.success
|
|
240
|
+
? validationResult.data[key]
|
|
241
|
+
: rawValue
|
|
242
|
+
|
|
243
|
+
const displayValue = isSecret ? maskValue(value) : String(value)
|
|
244
|
+
|
|
245
|
+
const errors = validationResult.success
|
|
246
|
+
? undefined
|
|
247
|
+
: validationResult.summary?.[key]
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
key,
|
|
251
|
+
value,
|
|
252
|
+
displayValue,
|
|
253
|
+
secret: isSecret,
|
|
254
|
+
valid: !errors,
|
|
255
|
+
errors,
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
success: validationResult.success,
|
|
261
|
+
params,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Formats an environment validation report into a readable table.
|
|
267
|
+
*
|
|
268
|
+
* @param {{
|
|
269
|
+
* success: boolean,
|
|
270
|
+
* params: Array<{
|
|
271
|
+
* key: string,
|
|
272
|
+
* displayValue: string,
|
|
273
|
+
* secret: boolean,
|
|
274
|
+
* valid: boolean,
|
|
275
|
+
* errors?: string[]
|
|
276
|
+
* }>
|
|
277
|
+
* }} report
|
|
278
|
+
*
|
|
279
|
+
* @returns {string}
|
|
280
|
+
*/
|
|
281
|
+
export function formatEnvReport(report) {
|
|
282
|
+
const headers = ['STATUS', 'KEY', 'VALUE', 'NOTES']
|
|
283
|
+
|
|
284
|
+
const rows = report.params.map((p) => {
|
|
285
|
+
const status = p.valid ? 'OK' : 'ERROR'
|
|
286
|
+
const notes = p.errors?.join('; ') || ''
|
|
287
|
+
|
|
288
|
+
return [status, p.key, p.displayValue, notes]
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const allRows = [headers, ...rows]
|
|
292
|
+
|
|
293
|
+
const colWidths = headers.map((_, i) =>
|
|
294
|
+
Math.max(...allRows.map((row) => row[i].length)),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const formatRow = (row) =>
|
|
298
|
+
row.map((cell, i) => cell.padEnd(colWidths[i])).join(' ')
|
|
299
|
+
|
|
300
|
+
const lines = allRows.map(formatRow)
|
|
301
|
+
|
|
302
|
+
return [
|
|
303
|
+
'Environment variables',
|
|
304
|
+
'',
|
|
305
|
+
...lines,
|
|
306
|
+
'',
|
|
307
|
+
report.success ? 'All variables are valid.' : 'Some variables are invalid.',
|
|
308
|
+
].join('\n')
|
|
309
|
+
}
|
|
@@ -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
|
-
|
|
45
|
+
try {
|
|
46
|
+
const missingTables = []
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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,132 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { buildEnvReport } from '../../src/env/env-validation.js'
|
|
4
|
+
|
|
5
|
+
const mask = (v) => `***${String(v).slice(-2)}`
|
|
6
|
+
|
|
7
|
+
describe('buildEnvReport', () => {
|
|
8
|
+
it('builds report for successful validation', () => {
|
|
9
|
+
const definition = {
|
|
10
|
+
PORT: { type: 'number' },
|
|
11
|
+
API_KEY: { type: 'string', secret: true },
|
|
12
|
+
DEBUG: { type: 'boolean' },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const values = {
|
|
16
|
+
PORT: 3000,
|
|
17
|
+
API_KEY: 'secret123',
|
|
18
|
+
DEBUG: false,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const validationResult = {
|
|
22
|
+
success: true,
|
|
23
|
+
data: values,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const report = buildEnvReport(definition, values, validationResult, mask)
|
|
27
|
+
|
|
28
|
+
expect(report.success).toBe(true)
|
|
29
|
+
expect(report.params).toHaveLength(3)
|
|
30
|
+
|
|
31
|
+
const apiKey = report.params.find((p) => p.key === 'API_KEY')
|
|
32
|
+
expect(apiKey.secret).toBe(true)
|
|
33
|
+
expect(apiKey.displayValue).toBe('***23')
|
|
34
|
+
expect(apiKey.valid).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('builds report for failed validation with errors', () => {
|
|
38
|
+
const definition = {
|
|
39
|
+
PORT: { type: 'number' },
|
|
40
|
+
DEBUG: { type: 'boolean' },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const values = {
|
|
44
|
+
PORT: 0,
|
|
45
|
+
DEBUG: false,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const validationResult = {
|
|
49
|
+
success: false,
|
|
50
|
+
summary: {
|
|
51
|
+
PORT: ['Invalid number'],
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const report = buildEnvReport(definition, values, validationResult, mask)
|
|
56
|
+
|
|
57
|
+
expect(report.success).toBe(false)
|
|
58
|
+
|
|
59
|
+
const port = report.params.find((p) => p.key === 'PORT')
|
|
60
|
+
expect(port.valid).toBe(false)
|
|
61
|
+
expect(port.errors).toEqual(['Invalid number'])
|
|
62
|
+
expect(port.displayValue).toBe('0')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('marks parameter as invalid when errors exist', () => {
|
|
66
|
+
const definition = {
|
|
67
|
+
PORT: { type: 'number' },
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const values = {
|
|
71
|
+
PORT: 'x',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const validationResult = {
|
|
75
|
+
success: false,
|
|
76
|
+
summary: {
|
|
77
|
+
PORT: ['Invalid number'],
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const report = buildEnvReport(definition, values, validationResult, mask)
|
|
82
|
+
|
|
83
|
+
const port = report.params[0]
|
|
84
|
+
expect(port.valid).toBe(false)
|
|
85
|
+
expect(port.errors).toEqual(['Invalid number'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('uses defaultMask when mask is not a function', () => {
|
|
89
|
+
const definition = {
|
|
90
|
+
API_KEY: { type: 'string', secret: true },
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const values = {
|
|
94
|
+
API_KEY: 'secret123',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const validationResult = {
|
|
98
|
+
success: true,
|
|
99
|
+
data: values,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const report = buildEnvReport(definition, values, validationResult, null)
|
|
103
|
+
|
|
104
|
+
const apiKey = report.params[0]
|
|
105
|
+
expect(apiKey.secret).toBe(true)
|
|
106
|
+
expect(typeof apiKey.displayValue).toBe('string')
|
|
107
|
+
expect(apiKey.displayValue).not.toBe('secret123')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('keeps params order as definition order', () => {
|
|
111
|
+
const definition = {
|
|
112
|
+
A: { type: 'string' },
|
|
113
|
+
B: { type: 'string' },
|
|
114
|
+
C: { type: 'string' },
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const values = {
|
|
118
|
+
A: '1',
|
|
119
|
+
B: '2',
|
|
120
|
+
C: '3',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const validationResult = {
|
|
124
|
+
success: true,
|
|
125
|
+
data: values,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const report = buildEnvReport(definition, values, validationResult, mask)
|
|
129
|
+
|
|
130
|
+
expect(report.params.map((p) => p.key)).toEqual(['A', 'B', 'C'])
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { createZodSchema } from '../../src/env/env-validation.js'
|
|
5
|
+
|
|
6
|
+
describe('createZodSchema', () => {
|
|
7
|
+
it('creates a Zod object schema from definition', () => {
|
|
8
|
+
const definition = {
|
|
9
|
+
PORT: { type: 'number', required: true },
|
|
10
|
+
DEBUG: { type: 'boolean' },
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const schema = createZodSchema(definition)
|
|
14
|
+
|
|
15
|
+
expect(schema).toBeInstanceOf(z.ZodObject)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('parses valid values successfully', () => {
|
|
19
|
+
const definition = {
|
|
20
|
+
PORT: { type: 'number', required: true, min: 1 },
|
|
21
|
+
DEBUG: { type: 'boolean', default: false },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const schema = createZodSchema(definition)
|
|
25
|
+
|
|
26
|
+
const result = schema.parse({
|
|
27
|
+
PORT: '3000',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({
|
|
31
|
+
PORT: 3000,
|
|
32
|
+
DEBUG: false,
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('throws error when required field is missing', () => {
|
|
37
|
+
const definition = {
|
|
38
|
+
PORT: { type: 'number', required: true },
|
|
39
|
+
DEBUG: { type: 'boolean' },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const schema = createZodSchema(definition)
|
|
43
|
+
|
|
44
|
+
expect(() => {
|
|
45
|
+
schema.parse({
|
|
46
|
+
DEBUG: true,
|
|
47
|
+
})
|
|
48
|
+
}).toThrow()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('allows optional fields to be missing', () => {
|
|
52
|
+
const definition = {
|
|
53
|
+
PORT: { type: 'number', required: true },
|
|
54
|
+
DEBUG: { type: 'boolean' },
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const schema = createZodSchema(definition)
|
|
58
|
+
|
|
59
|
+
const result = schema.parse({
|
|
60
|
+
PORT: 8080,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
PORT: 8080,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('applies string constraints correctly', () => {
|
|
69
|
+
const definition = {
|
|
70
|
+
NODE_ENV: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
required: true,
|
|
73
|
+
enum: ['development', 'production'],
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const schema = createZodSchema(definition)
|
|
78
|
+
|
|
79
|
+
expect(() => {
|
|
80
|
+
schema.parse({ NODE_ENV: 'prod' })
|
|
81
|
+
}).toThrow()
|
|
82
|
+
|
|
83
|
+
const result = schema.parse({ NODE_ENV: 'production' })
|
|
84
|
+
expect(result.NODE_ENV).toBe('production')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('applies number constraints correctly', () => {
|
|
88
|
+
const definition = {
|
|
89
|
+
PORT: {
|
|
90
|
+
type: 'number',
|
|
91
|
+
required: true,
|
|
92
|
+
int: true,
|
|
93
|
+
min: 1,
|
|
94
|
+
max: 65535,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const schema = createZodSchema(definition)
|
|
99
|
+
|
|
100
|
+
expect(() => {
|
|
101
|
+
schema.parse({ PORT: 0 })
|
|
102
|
+
}).toThrow()
|
|
103
|
+
|
|
104
|
+
expect(() => {
|
|
105
|
+
schema.parse({ PORT: 70000 })
|
|
106
|
+
}).toThrow()
|
|
107
|
+
|
|
108
|
+
const result = schema.parse({ PORT: '3000' })
|
|
109
|
+
expect(result.PORT).toBe(3000)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('validates string formats', () => {
|
|
113
|
+
const definition = {
|
|
114
|
+
DATABASE_URL: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
required: true,
|
|
117
|
+
format: 'url',
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const schema = createZodSchema(definition)
|
|
122
|
+
|
|
123
|
+
expect(() => {
|
|
124
|
+
schema.parse({ DATABASE_URL: 'not-a-url' })
|
|
125
|
+
}).toThrow()
|
|
126
|
+
|
|
127
|
+
const result = schema.parse({
|
|
128
|
+
DATABASE_URL: 'https://example.com',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(result.DATABASE_URL).toBe('https://example.com')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('collects multiple validation errors', () => {
|
|
135
|
+
const definition = {
|
|
136
|
+
PORT: { type: 'number', required: true, min: 1 },
|
|
137
|
+
NODE_ENV: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
required: true,
|
|
140
|
+
enum: ['development', 'production'],
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const schema = createZodSchema(definition)
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
schema.parse({
|
|
148
|
+
PORT: 0,
|
|
149
|
+
NODE_ENV: 'prod',
|
|
150
|
+
})
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// @ts-ignore
|
|
153
|
+
expect(error.issues).toHaveLength(2)
|
|
154
|
+
|
|
155
|
+
// @ts-ignore
|
|
156
|
+
const paths = error.issues.map((i) => i.path[0])
|
|
157
|
+
expect(paths).toContain('PORT')
|
|
158
|
+
expect(paths).toContain('NODE_ENV')
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('coerces boolean values correctly', () => {
|
|
163
|
+
const definition = {
|
|
164
|
+
DEBUG: { type: 'boolean', required: true },
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const schema = createZodSchema(definition)
|
|
168
|
+
|
|
169
|
+
const result = schema.parse({
|
|
170
|
+
DEBUG: 'true',
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
expect(result.DEBUG).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import {
|
|
3
|
+
validateEnv,
|
|
4
|
+
buildEnvReport,
|
|
5
|
+
formatEnvReport,
|
|
6
|
+
} from '../../src/env/env-validation.js'
|
|
7
|
+
|
|
8
|
+
// mask example
|
|
9
|
+
const mask = (value) => {
|
|
10
|
+
if (value == null) {
|
|
11
|
+
return '******'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const str = String(value)
|
|
15
|
+
if (str.length <= 4) {
|
|
16
|
+
return '****'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `${str.slice(0, 2)}****${str.slice(-2)}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Definitions
|
|
24
|
+
*/
|
|
25
|
+
const definition = {
|
|
26
|
+
PORT: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
required: true,
|
|
29
|
+
int: true,
|
|
30
|
+
min: 1,
|
|
31
|
+
max: 65535,
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
TIMEOUT: {
|
|
35
|
+
type: 'number',
|
|
36
|
+
min: 100,
|
|
37
|
+
max: 10000,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
RETRIES: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
int: true,
|
|
43
|
+
min: 0,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
DEBUG: {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
MODE: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
enum: ['development', 'production'],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
API_KEY: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
secret: true,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Values scenarios
|
|
63
|
+
*/
|
|
64
|
+
const scenarios = [
|
|
65
|
+
{
|
|
66
|
+
title: '✅ All valid values',
|
|
67
|
+
values: {
|
|
68
|
+
PORT: 3000,
|
|
69
|
+
TIMEOUT: 500,
|
|
70
|
+
RETRIES: 3,
|
|
71
|
+
DEBUG: false,
|
|
72
|
+
MODE: 'development',
|
|
73
|
+
API_KEY: 'secret123',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
title: '❌ Number too small (PORT)',
|
|
79
|
+
values: {
|
|
80
|
+
PORT: 0,
|
|
81
|
+
TIMEOUT: 500,
|
|
82
|
+
RETRIES: 3,
|
|
83
|
+
DEBUG: true,
|
|
84
|
+
MODE: 'production',
|
|
85
|
+
API_KEY: 'secret123',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
title: '❌ Number too large (TIMEOUT)',
|
|
91
|
+
values: {
|
|
92
|
+
PORT: 3000,
|
|
93
|
+
TIMEOUT: 20000,
|
|
94
|
+
RETRIES: 1,
|
|
95
|
+
DEBUG: false,
|
|
96
|
+
MODE: 'development',
|
|
97
|
+
API_KEY: 'secret123',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
title: '❌ Invalid number (string)',
|
|
103
|
+
values: {
|
|
104
|
+
PORT: 'abc',
|
|
105
|
+
TIMEOUT: 500,
|
|
106
|
+
RETRIES: 2,
|
|
107
|
+
DEBUG: false,
|
|
108
|
+
MODE: 'development',
|
|
109
|
+
API_KEY: 'secret123',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
title: '❌ Invalid enum (MODE)',
|
|
115
|
+
values: {
|
|
116
|
+
PORT: 3000,
|
|
117
|
+
TIMEOUT: 500,
|
|
118
|
+
RETRIES: 2,
|
|
119
|
+
DEBUG: false,
|
|
120
|
+
MODE: 'prod',
|
|
121
|
+
API_KEY: 'secret123',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
{
|
|
126
|
+
title: '✅ Boolean coercion + missing secret',
|
|
127
|
+
values: {
|
|
128
|
+
PORT: '8080',
|
|
129
|
+
TIMEOUT: '1000',
|
|
130
|
+
RETRIES: '0',
|
|
131
|
+
DEBUG: 'true',
|
|
132
|
+
MODE: 'production',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Run scenarios
|
|
139
|
+
*/
|
|
140
|
+
for (const scenario of scenarios) {
|
|
141
|
+
console.log('\n===================================================')
|
|
142
|
+
console.log(scenario.title)
|
|
143
|
+
console.log('===================================================\n')
|
|
144
|
+
|
|
145
|
+
const validationResult = validateEnv(definition, scenario.values)
|
|
146
|
+
const report = buildEnvReport(
|
|
147
|
+
definition,
|
|
148
|
+
scenario.values,
|
|
149
|
+
validationResult,
|
|
150
|
+
mask,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
console.log(formatEnvReport(report))
|
|
154
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { validateEnv } from '../../src/env/env-validation.js'
|
|
3
|
+
|
|
4
|
+
describe('validateEnv', () => {
|
|
5
|
+
it('returns success result when values are valid', () => {
|
|
6
|
+
const definition = {
|
|
7
|
+
PORT: { type: 'number', required: true, min: 1 },
|
|
8
|
+
DEBUG: { type: 'boolean', default: false },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const values = {
|
|
12
|
+
PORT: '3000',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const result = validateEnv(definition, values)
|
|
16
|
+
|
|
17
|
+
expect(result.success).toBe(true)
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
expect(result.data).toEqual({
|
|
20
|
+
PORT: 3000,
|
|
21
|
+
DEBUG: false,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns error result with summary when required field is missing', () => {
|
|
26
|
+
const definition = {
|
|
27
|
+
PORT: { type: 'number', required: true },
|
|
28
|
+
DEBUG: { type: 'boolean' },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const values = {
|
|
32
|
+
DEBUG: true,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = validateEnv(definition, values)
|
|
36
|
+
|
|
37
|
+
expect(result.success).toBe(false)
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
expect(result.summary).toBeDefined()
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
expect(result.summary).toHaveProperty('PORT')
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
expect(result.summary.PORT.length).toBeGreaterThan(0)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns summary with errors for the field when value is invalid', () => {
|
|
47
|
+
const definition = {
|
|
48
|
+
PORT: {
|
|
49
|
+
type: 'number',
|
|
50
|
+
required: true,
|
|
51
|
+
int: true,
|
|
52
|
+
min: 10,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const values = {
|
|
57
|
+
PORT: '5.5',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = validateEnv(definition, values)
|
|
61
|
+
|
|
62
|
+
expect(result.success).toBe(false)
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
expect(result.summary).toHaveProperty('PORT')
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
expect(result.summary.PORT.length).toBeGreaterThan(0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns summary with errors from multiple fields', () => {
|
|
70
|
+
const definition = {
|
|
71
|
+
PORT: { type: 'number', required: true, min: 1 },
|
|
72
|
+
NODE_ENV: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
required: true,
|
|
75
|
+
enum: ['development', 'production'],
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const values = {
|
|
80
|
+
PORT: 0,
|
|
81
|
+
NODE_ENV: 'prod',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = validateEnv(definition, values)
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(false)
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
expect(result.summary).toHaveProperty('PORT')
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
expect(result.summary).toHaveProperty('NODE_ENV')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('does not include summary when validation succeeds', () => {
|
|
94
|
+
const definition = {
|
|
95
|
+
PORT: { type: 'number', required: true },
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const values = {
|
|
99
|
+
PORT: 8080,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = validateEnv(definition, values)
|
|
103
|
+
|
|
104
|
+
expect(result.success).toBe(true)
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
expect(result.summary).toBeUndefined()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a single field definition into a Zod schema.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} def
|
|
5
|
+
* @param {'string'|'number'|'boolean'} def.type
|
|
6
|
+
*
|
|
7
|
+
* @param {boolean} [def.required]
|
|
8
|
+
* Whether the value is required. Defaults to false.
|
|
9
|
+
*
|
|
10
|
+
* @param {*} [def.default]
|
|
11
|
+
* Default value if the variable is missing.
|
|
12
|
+
*
|
|
13
|
+
* @param {boolean} [def.secret]
|
|
14
|
+
* Marks the variable as secret (used only for masking/logging).
|
|
15
|
+
*
|
|
16
|
+
* // string options
|
|
17
|
+
* @param {number} [def.minLength]
|
|
18
|
+
* @param {number} [def.maxLength]
|
|
19
|
+
* @param {string} [def.pattern]
|
|
20
|
+
* @param {string[]} [def.enum]
|
|
21
|
+
* @param {'email'|'url'} [def.format]
|
|
22
|
+
*
|
|
23
|
+
* // number options
|
|
24
|
+
* @param {number} [def.min]
|
|
25
|
+
* @param {number} [def.max]
|
|
26
|
+
* @param {boolean} [def.int]
|
|
27
|
+
* @param {boolean} [def.positive]
|
|
28
|
+
* @param {boolean} [def.negative]
|
|
29
|
+
*
|
|
30
|
+
* @returns {import('zod').ZodTypeAny}
|
|
31
|
+
*/
|
|
32
|
+
export function defToZod(def: {
|
|
33
|
+
type: 'string' | 'number' | 'boolean'
|
|
34
|
+
required?: boolean
|
|
35
|
+
default?: any
|
|
36
|
+
secret?: boolean
|
|
37
|
+
minLength?: number
|
|
38
|
+
maxLength?: number
|
|
39
|
+
pattern?: string
|
|
40
|
+
enum?: string[]
|
|
41
|
+
format?: 'email' | 'url'
|
|
42
|
+
min?: number
|
|
43
|
+
max?: number
|
|
44
|
+
int?: boolean
|
|
45
|
+
positive?: boolean
|
|
46
|
+
negative?: boolean
|
|
47
|
+
}): import('zod').ZodTypeAny
|
|
48
|
+
/**
|
|
49
|
+
* Builds a Zod object schema from a JSON definition map.
|
|
50
|
+
*
|
|
51
|
+
* @param {Record<string, Object>} definition
|
|
52
|
+
* Map of environment variable names to field definitions.
|
|
53
|
+
*
|
|
54
|
+
* @returns {import('zod').ZodObject<any>}
|
|
55
|
+
*/
|
|
56
|
+
export function createZodSchema(
|
|
57
|
+
definition: Record<string, any>,
|
|
58
|
+
): import('zod').ZodObject<any>
|
|
59
|
+
/**
|
|
60
|
+
* Validates values using a JSON definition and Zod.
|
|
61
|
+
*
|
|
62
|
+
* @param {Record<string, Object>} definition
|
|
63
|
+
* @param {Record<string, any>} values
|
|
64
|
+
*
|
|
65
|
+
* @returns {{
|
|
66
|
+
* success: true,
|
|
67
|
+
* data: Record<string, any>
|
|
68
|
+
* } | {
|
|
69
|
+
* success: false,
|
|
70
|
+
* summary: Record<string, string[]>
|
|
71
|
+
* }}
|
|
72
|
+
*/
|
|
73
|
+
export function validateEnv(
|
|
74
|
+
definition: Record<string, any>,
|
|
75
|
+
values: Record<string, any>,
|
|
76
|
+
):
|
|
77
|
+
| {
|
|
78
|
+
success: true
|
|
79
|
+
data: Record<string, any>
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
success: false
|
|
83
|
+
summary: Record<string, string[]>
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds a structured environment validation report.
|
|
87
|
+
*
|
|
88
|
+
* @param {Record<string, Object>} definition
|
|
89
|
+
* @param {Record<string, any>} values
|
|
90
|
+
* Raw input values (e.g. process.env)
|
|
91
|
+
*
|
|
92
|
+
* @param {{
|
|
93
|
+
* success: boolean,
|
|
94
|
+
* data?: Record<string, any>,
|
|
95
|
+
* summary?: Record<string, string[]>
|
|
96
|
+
* }} validationResult
|
|
97
|
+
*
|
|
98
|
+
* @param {(value: any) => string} mask
|
|
99
|
+
*
|
|
100
|
+
* @returns {{
|
|
101
|
+
* success: boolean,
|
|
102
|
+
* params: Array<{
|
|
103
|
+
* key: string,
|
|
104
|
+
* value: any,
|
|
105
|
+
* displayValue: string,
|
|
106
|
+
* secret: boolean,
|
|
107
|
+
* valid: boolean,
|
|
108
|
+
* errors?: string[]
|
|
109
|
+
* }>
|
|
110
|
+
* }}
|
|
111
|
+
*/
|
|
112
|
+
export function buildEnvReport(
|
|
113
|
+
definition: Record<string, any>,
|
|
114
|
+
values: Record<string, any>,
|
|
115
|
+
validationResult: {
|
|
116
|
+
success: boolean
|
|
117
|
+
data?: Record<string, any>
|
|
118
|
+
summary?: Record<string, string[]>
|
|
119
|
+
},
|
|
120
|
+
mask: (value: any) => string,
|
|
121
|
+
): {
|
|
122
|
+
success: boolean
|
|
123
|
+
params: Array<{
|
|
124
|
+
key: string
|
|
125
|
+
value: any
|
|
126
|
+
displayValue: string
|
|
127
|
+
secret: boolean
|
|
128
|
+
valid: boolean
|
|
129
|
+
errors?: string[]
|
|
130
|
+
}>
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Formats an environment validation report into a readable table.
|
|
134
|
+
*
|
|
135
|
+
* @param {{
|
|
136
|
+
* success: boolean,
|
|
137
|
+
* params: Array<{
|
|
138
|
+
* key: string,
|
|
139
|
+
* displayValue: string,
|
|
140
|
+
* secret: boolean,
|
|
141
|
+
* valid: boolean,
|
|
142
|
+
* errors?: string[]
|
|
143
|
+
* }>
|
|
144
|
+
* }} report
|
|
145
|
+
*
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function formatEnvReport(report: {
|
|
149
|
+
success: boolean
|
|
150
|
+
params: Array<{
|
|
151
|
+
key: string
|
|
152
|
+
displayValue: string
|
|
153
|
+
secret: boolean
|
|
154
|
+
valid: boolean
|
|
155
|
+
errors?: string[]
|
|
156
|
+
}>
|
|
157
|
+
}): string
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Applies ORDER BY clause.
|
|
2
|
+
* Applies ORDER BY clause(s) to a Knex query builder.
|
|
3
|
+
*
|
|
4
|
+
* Supports a single orderBy object or an array of orderBy objects.
|
|
5
|
+
* Validates order direction to prevent invalid SQL.
|
|
3
6
|
*
|
|
4
7
|
* @param {Object} params
|
|
5
|
-
* @param {import('knex').Knex.QueryBuilder} params.query
|
|
6
|
-
* @param {
|
|
8
|
+
* @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance
|
|
9
|
+
* @param {OrderByItem | OrderByItem[]} params.orderBy - Order by definition(s)
|
|
10
|
+
* @returns {import('knex').Knex.QueryBuilder} The modified query builder
|
|
7
11
|
*/
|
|
8
12
|
export function applyOrderBy({
|
|
9
13
|
query,
|
|
10
14
|
orderBy,
|
|
11
15
|
}: {
|
|
12
16
|
query: import('knex').Knex.QueryBuilder
|
|
13
|
-
orderBy:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
orderBy: OrderByItem | OrderByItem[]
|
|
18
|
+
}): import('knex').Knex.QueryBuilder
|
|
19
|
+
export type OrderByItem = {
|
|
20
|
+
/**
|
|
21
|
+
* - Column name to order by
|
|
22
|
+
*/
|
|
23
|
+
column: string
|
|
24
|
+
/**
|
|
25
|
+
* - Order direction
|
|
26
|
+
*/
|
|
27
|
+
direction?: 'asc' | 'desc'
|
|
28
|
+
}
|