@dbos-inc/koa-serve 3.5.44-preview.gc094fdab44 → 3.6.5-preview
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/dist/src/dboshttp.d.ts +54 -0
- package/dist/src/dboshttp.d.ts.map +1 -1
- package/dist/src/dboshttp.js +418 -4
- package/dist/src/dboshttp.js.map +1 -1
- package/dist/src/dboskoa.js +1 -1
- package/dist/src/dboskoa.js.map +1 -1
- package/dist/src/index.d.ts +9 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -3
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/dboshttp.ts +523 -4
- package/src/dboskoa.ts +1 -1
- package/src/index.ts +17 -0
- package/tests/argsource.test.ts +150 -0
- package/tests/auth.test.ts +26 -4
- package/tests/endpoints.test.ts +58 -34
- package/tests/steps.test.ts +5 -0
- package/tests/transactions.test.ts +5 -0
- package/tests/validation.test.ts +531 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbos-inc/koa-serve",
|
|
3
|
-
"version": "3.5
|
|
3
|
+
"version": "3.6.5-preview",
|
|
4
4
|
"description": "DBOS HTTP Package for serving workflows with Koa",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@dbos-inc/dbos-sdk": "*",
|
|
34
|
-
"@dbos-inc/knex-datasource": "*"
|
|
34
|
+
"@dbos-inc/knex-datasource": "*",
|
|
35
|
+
"reflect-metadata": "^0.1.14 || ^0.2.0"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"@koa/bodyparser": "5.0.0",
|
package/src/dboshttp.ts
CHANGED
|
@@ -1,8 +1,469 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
1
2
|
import { IncomingHttpHeaders } from 'http';
|
|
2
3
|
import { ParsedUrlQuery } from 'querystring';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { DBOSMethodMiddlewareInstaller, MethodRegistrationBase } from '@dbos-inc/dbos-sdk';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
4
7
|
|
|
5
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ArgDataType,
|
|
10
|
+
DBOS,
|
|
11
|
+
DBOSDataType,
|
|
12
|
+
DBOSLifecycleCallback,
|
|
13
|
+
Error as DBOSErrors,
|
|
14
|
+
MethodParameter,
|
|
15
|
+
} from '@dbos-inc/dbos-sdk';
|
|
16
|
+
|
|
17
|
+
const VALIDATOR = 'validator';
|
|
18
|
+
|
|
19
|
+
export enum ArgRequiredOptions {
|
|
20
|
+
REQUIRED = 'REQUIRED',
|
|
21
|
+
OPTIONAL = 'OPTIONAL',
|
|
22
|
+
DEFAULT = 'DEFAULT',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ValidatorClassInfo {
|
|
26
|
+
defaultArgRequired?: ArgRequiredOptions;
|
|
27
|
+
defaultArgValidate?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ValidatorFuncInfo {
|
|
31
|
+
performArgValidation?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ValidatorArgInfo {
|
|
35
|
+
required?: ArgRequiredOptions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getValidatorClassInfo(methReg: MethodRegistrationBase) {
|
|
39
|
+
const valInfo = methReg.defaults?.getRegisteredInfo(VALIDATOR) as ValidatorClassInfo;
|
|
40
|
+
return {
|
|
41
|
+
defaultArgRequired: valInfo?.defaultArgRequired ?? ArgRequiredOptions.DEFAULT,
|
|
42
|
+
defaultArgValidate: valInfo?.defaultArgValidate ?? false,
|
|
43
|
+
} satisfies ValidatorClassInfo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function requestArgValidation(methReg: MethodRegistrationBase) {
|
|
47
|
+
(methReg.getRegisteredInfo(VALIDATOR) as ValidatorFuncInfo).performArgValidation = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getValidatorFuncInfo(methReg: MethodRegistrationBase) {
|
|
51
|
+
const valInfo = methReg.getRegisteredInfo(VALIDATOR) as ValidatorFuncInfo;
|
|
52
|
+
return {
|
|
53
|
+
performArgValidation: valInfo.performArgValidation ?? false,
|
|
54
|
+
} satisfies ValidatorFuncInfo;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getValidatorArgInfo(param: MethodParameter) {
|
|
58
|
+
const valInfo = param.getRegisteredInfo(VALIDATOR) as ValidatorArgInfo;
|
|
59
|
+
return {
|
|
60
|
+
required: valInfo.required ?? ArgRequiredOptions.DEFAULT,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class ValidationMiddleware implements DBOSMethodMiddlewareInstaller {
|
|
65
|
+
installMiddleware(methReg: MethodRegistrationBase): void {
|
|
66
|
+
const valInfo = getValidatorClassInfo(methReg);
|
|
67
|
+
const defaultArgRequired = valInfo.defaultArgRequired;
|
|
68
|
+
const defaultArgValidate = valInfo.defaultArgValidate;
|
|
69
|
+
|
|
70
|
+
let shouldValidate =
|
|
71
|
+
getValidatorFuncInfo(methReg).performArgValidation ||
|
|
72
|
+
defaultArgRequired === ArgRequiredOptions.REQUIRED ||
|
|
73
|
+
defaultArgValidate;
|
|
74
|
+
|
|
75
|
+
for (const a of methReg.args) {
|
|
76
|
+
if (getValidatorArgInfo(a).required === ArgRequiredOptions.REQUIRED) {
|
|
77
|
+
shouldValidate = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (shouldValidate) {
|
|
82
|
+
requestArgValidation(methReg);
|
|
83
|
+
methReg.addEntryInterceptor(validateMethodArgs, 20);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const validationMiddleware = new ValidationMiddleware();
|
|
89
|
+
|
|
90
|
+
function validateMethodArgs<Args extends unknown[]>(methReg: MethodRegistrationBase, args: Args) {
|
|
91
|
+
const validationError = (msg: string) => {
|
|
92
|
+
const err = new DBOSErrors.DBOSDataValidationError(msg);
|
|
93
|
+
DBOS.span?.addEvent('DataValidationError', { message: err.message });
|
|
94
|
+
return err;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Input validation
|
|
98
|
+
methReg.args.forEach((argDescriptor, idx) => {
|
|
99
|
+
let argValue = args[idx];
|
|
100
|
+
|
|
101
|
+
// So... there is such a thing as "undefined", and another thing called "null"
|
|
102
|
+
// We will fold this to "undefined" for our APIs. It's just a rule of ours.
|
|
103
|
+
if (argValue === null) {
|
|
104
|
+
argValue = undefined;
|
|
105
|
+
args[idx] = undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (argValue === undefined) {
|
|
109
|
+
const valInfo = getValidatorClassInfo(methReg);
|
|
110
|
+
const defaultArgRequired = valInfo.defaultArgRequired;
|
|
111
|
+
const defaultArgValidate = valInfo.defaultArgValidate;
|
|
112
|
+
const argRequired = getValidatorArgInfo(argDescriptor).required;
|
|
113
|
+
if (
|
|
114
|
+
argRequired === ArgRequiredOptions.REQUIRED ||
|
|
115
|
+
(argRequired === ArgRequiredOptions.DEFAULT &&
|
|
116
|
+
(defaultArgRequired === ArgRequiredOptions.REQUIRED || defaultArgValidate))
|
|
117
|
+
) {
|
|
118
|
+
if (idx >= args.length) {
|
|
119
|
+
throw validationError(
|
|
120
|
+
`Insufficient number of arguments calling ${methReg.name} - ${args.length}/${methReg.args.length}`,
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
throw validationError(`Missing required argument ${argDescriptor.name} of ${methReg.name}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (argValue === undefined) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (argValue instanceof String) {
|
|
133
|
+
argValue = argValue.toString();
|
|
134
|
+
args[idx] = argValue;
|
|
135
|
+
}
|
|
136
|
+
if (argValue instanceof Boolean) {
|
|
137
|
+
argValue = argValue.valueOf();
|
|
138
|
+
args[idx] = argValue;
|
|
139
|
+
}
|
|
140
|
+
if (argValue instanceof Number) {
|
|
141
|
+
argValue = argValue.valueOf();
|
|
142
|
+
args[idx] = argValue;
|
|
143
|
+
}
|
|
144
|
+
if (argValue instanceof BigInt) {
|
|
145
|
+
// ES2020+
|
|
146
|
+
argValue = argValue.valueOf();
|
|
147
|
+
args[idx] = argValue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Argument validation - below - if we have any info about it
|
|
151
|
+
if (!argDescriptor.dataType) return;
|
|
152
|
+
|
|
153
|
+
// Maybe look into https://www.npmjs.com/package/validator
|
|
154
|
+
// We could support emails and other validations too with something like that...
|
|
155
|
+
if (argDescriptor.dataType.dataType === 'text' || argDescriptor.dataType.dataType === 'varchar') {
|
|
156
|
+
if (typeof argValue !== 'string') {
|
|
157
|
+
throw validationError(
|
|
158
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' and should be a string`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (argDescriptor.dataType.length > 0) {
|
|
162
|
+
if (argValue.length > argDescriptor.dataType.length) {
|
|
163
|
+
throw validationError(
|
|
164
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' with maximum length ${argDescriptor.dataType.length} but has length ${argValue.length}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (argDescriptor.dataType.dataType === 'boolean') {
|
|
170
|
+
if (typeof argValue !== 'boolean') {
|
|
171
|
+
if (typeof argValue === 'number') {
|
|
172
|
+
if (argValue === 0 || argValue === 1) {
|
|
173
|
+
argValue = argValue !== 0 ? true : false;
|
|
174
|
+
args[idx] = argValue;
|
|
175
|
+
} else {
|
|
176
|
+
throw validationError(
|
|
177
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' and may be a number (0 or 1) convertible to boolean, but was ${argValue}.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} else if (typeof argValue === 'string') {
|
|
181
|
+
if (argValue.toLowerCase() === 't' || argValue.toLowerCase() === 'true' || argValue === '1') {
|
|
182
|
+
argValue = true;
|
|
183
|
+
args[idx] = argValue;
|
|
184
|
+
} else if (argValue.toLowerCase() === 'f' || argValue.toLowerCase() === 'false' || argValue === '0') {
|
|
185
|
+
argValue = false;
|
|
186
|
+
args[idx] = argValue;
|
|
187
|
+
} else {
|
|
188
|
+
throw validationError(
|
|
189
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' and may be a string convertible to boolean, but was ${argValue}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
throw validationError(
|
|
194
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' and should be a boolean`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (argDescriptor.dataType.dataType === 'decimal') {
|
|
200
|
+
// Range check precision and scale... wishing there was a bigdecimal
|
|
201
|
+
// Floats don't really permit us to check the scale.
|
|
202
|
+
if (typeof argValue !== 'number') {
|
|
203
|
+
throw validationError(
|
|
204
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' and should be a number`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
let prec = argDescriptor.dataType.precision;
|
|
208
|
+
if (prec > 0) {
|
|
209
|
+
if (argDescriptor.dataType.scale > 0) {
|
|
210
|
+
prec = prec - argDescriptor.dataType.scale;
|
|
211
|
+
}
|
|
212
|
+
if (Math.abs(argValue) >= Math.exp(prec)) {
|
|
213
|
+
throw validationError(
|
|
214
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is out of range for type '${argDescriptor.dataType.formatAsString()}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (argDescriptor.dataType.dataType === 'double' || argDescriptor.dataType.dataType === 'integer') {
|
|
220
|
+
if (typeof argValue !== 'number') {
|
|
221
|
+
if (typeof argValue === 'string') {
|
|
222
|
+
const n = parseFloat(argValue);
|
|
223
|
+
if (isNaN(n)) {
|
|
224
|
+
throw validationError(
|
|
225
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' and should be a number`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
argValue = n;
|
|
229
|
+
args[idx] = argValue;
|
|
230
|
+
} else if (typeof argValue === 'bigint') {
|
|
231
|
+
// Hum, maybe we should allow bigint as a type, number won't even do 64-bit.
|
|
232
|
+
argValue = Number(argValue).valueOf();
|
|
233
|
+
args[idx] = argValue;
|
|
234
|
+
} else {
|
|
235
|
+
throw validationError(
|
|
236
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' and should be a number`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (argDescriptor.dataType.dataType === 'integer') {
|
|
241
|
+
if (!Number.isInteger(argValue)) {
|
|
242
|
+
throw validationError(
|
|
243
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' but has a fractional part`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (argDescriptor.dataType.dataType === 'timestamp') {
|
|
249
|
+
if (!(argValue instanceof Date)) {
|
|
250
|
+
if (typeof argValue === 'string') {
|
|
251
|
+
const d = Date.parse(argValue);
|
|
252
|
+
if (isNaN(d)) {
|
|
253
|
+
throw validationError(
|
|
254
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' but is a string that will not parse as Date`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
argValue = new Date(d);
|
|
258
|
+
args[idx] = argValue;
|
|
259
|
+
} else {
|
|
260
|
+
throw validationError(
|
|
261
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' but is not a date or time`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (argDescriptor.dataType.dataType === 'uuid') {
|
|
267
|
+
// This validation is loose. A tighter one would be:
|
|
268
|
+
// /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
|
|
269
|
+
// That matches UUID version 1-5.
|
|
270
|
+
if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(String(argValue))) {
|
|
271
|
+
throw validationError(
|
|
272
|
+
`Argument ${argDescriptor.name} of ${methReg.name} is marked as type '${argDescriptor.dataType.dataType}' but is not a valid UUID`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// JSON can be anything. We can validate it against a schema at some later version...
|
|
277
|
+
});
|
|
278
|
+
return args;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function ArgRequired(target: object, propertyKey: PropertyKey, param: number) {
|
|
282
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
283
|
+
VALIDATOR,
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
285
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
286
|
+
{
|
|
287
|
+
ctorOrProto: target,
|
|
288
|
+
name: propertyKey.toString(),
|
|
289
|
+
param,
|
|
290
|
+
},
|
|
291
|
+
) as ValidatorArgInfo;
|
|
292
|
+
|
|
293
|
+
curParam.required = ArgRequiredOptions.REQUIRED;
|
|
294
|
+
|
|
295
|
+
DBOS.registerMiddlewareInstaller(validationMiddleware);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function ArgOptional(target: object, propertyKey: PropertyKey, param: number) {
|
|
299
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
300
|
+
VALIDATOR,
|
|
301
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
302
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
303
|
+
{
|
|
304
|
+
ctorOrProto: target,
|
|
305
|
+
name: propertyKey.toString(),
|
|
306
|
+
param,
|
|
307
|
+
},
|
|
308
|
+
) as ValidatorArgInfo;
|
|
309
|
+
|
|
310
|
+
curParam.required = ArgRequiredOptions.OPTIONAL;
|
|
311
|
+
|
|
312
|
+
DBOS.registerMiddlewareInstaller(validationMiddleware);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function ArgDate() {
|
|
316
|
+
// TODO a little more info about it - is it a date or timestamp precision?
|
|
317
|
+
return function (target: object, propertyKey: PropertyKey, param: number) {
|
|
318
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
319
|
+
'type',
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
321
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
322
|
+
{
|
|
323
|
+
ctorOrProto: target,
|
|
324
|
+
name: propertyKey.toString(),
|
|
325
|
+
param,
|
|
326
|
+
},
|
|
327
|
+
) as ArgDataType;
|
|
328
|
+
|
|
329
|
+
if (!curParam.dataType) curParam.dataType = new DBOSDataType();
|
|
330
|
+
curParam.dataType.dataType = 'timestamp';
|
|
331
|
+
|
|
332
|
+
DBOS.registerMiddlewareInstaller(validationMiddleware);
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function ArgVarchar(length: number) {
|
|
337
|
+
return function (target: object, propertyKey: PropertyKey, param: number) {
|
|
338
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
339
|
+
'type',
|
|
340
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
341
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
342
|
+
{
|
|
343
|
+
ctorOrProto: target,
|
|
344
|
+
name: propertyKey.toString(),
|
|
345
|
+
param,
|
|
346
|
+
},
|
|
347
|
+
) as ArgDataType;
|
|
348
|
+
|
|
349
|
+
curParam.dataType = DBOSDataType.varchar(length);
|
|
350
|
+
|
|
351
|
+
DBOS.registerMiddlewareInstaller(validationMiddleware);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function DefaultArgRequired<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
356
|
+
const clsreg = DBOS.associateClassWithInfo(VALIDATOR, ctor) as ValidatorClassInfo;
|
|
357
|
+
clsreg.defaultArgRequired = ArgRequiredOptions.REQUIRED;
|
|
358
|
+
|
|
359
|
+
DBOS.registerMiddlewareInstaller(validationMiddleware);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function DefaultArgValidate<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
363
|
+
const clsreg = DBOS.associateClassWithInfo(VALIDATOR, ctor) as ValidatorClassInfo;
|
|
364
|
+
clsreg.defaultArgValidate = true;
|
|
365
|
+
|
|
366
|
+
DBOS.registerMiddlewareInstaller(validationMiddleware);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function DefaultArgOptional<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
370
|
+
const clsreg = DBOS.associateClassWithInfo(VALIDATOR, ctor) as ValidatorClassInfo;
|
|
371
|
+
clsreg.defaultArgRequired = ArgRequiredOptions.OPTIONAL;
|
|
372
|
+
|
|
373
|
+
DBOS.registerMiddlewareInstaller(validationMiddleware);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export enum LogMasks {
|
|
377
|
+
NONE = 'NONE',
|
|
378
|
+
HASH = 'HASH',
|
|
379
|
+
SKIP = 'SKIP',
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
interface LoggerArgInfo {
|
|
383
|
+
logMask?: LogMasks;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export const LOGGER = 'log';
|
|
387
|
+
|
|
388
|
+
export function SkipLogging(target: object, propertyKey: PropertyKey, param: number) {
|
|
389
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
390
|
+
LOGGER,
|
|
391
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
392
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
393
|
+
{
|
|
394
|
+
ctorOrProto: target,
|
|
395
|
+
name: propertyKey.toString(),
|
|
396
|
+
param,
|
|
397
|
+
},
|
|
398
|
+
) as LoggerArgInfo;
|
|
399
|
+
|
|
400
|
+
curParam.logMask = LogMasks.SKIP;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function LogMask(mask: LogMasks) {
|
|
404
|
+
return function (target: object, propertyKey: PropertyKey, param: number) {
|
|
405
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
406
|
+
LOGGER,
|
|
407
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
408
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
409
|
+
{
|
|
410
|
+
ctorOrProto: target,
|
|
411
|
+
name: propertyKey.toString(),
|
|
412
|
+
param,
|
|
413
|
+
},
|
|
414
|
+
) as LoggerArgInfo;
|
|
415
|
+
|
|
416
|
+
curParam.logMask = mask;
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function generateSaltedHash(data: string, salt: string): string {
|
|
421
|
+
const hash = crypto.createHash('sha256'); // You can use other algorithms like 'md5', 'sha512', etc.
|
|
422
|
+
hash.update(data + salt);
|
|
423
|
+
return hash.digest('hex');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getLoggerArgInfo(param: MethodParameter) {
|
|
427
|
+
const valInfo = param.getRegisteredInfo(LOGGER) as LoggerArgInfo;
|
|
428
|
+
return {
|
|
429
|
+
logMask: valInfo.logMask ?? LogMasks.NONE,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
class LoggingMiddleware implements DBOSMethodMiddlewareInstaller {
|
|
434
|
+
installMiddleware(methReg: MethodRegistrationBase): void {
|
|
435
|
+
methReg.addEntryInterceptor(logMethodArgs, 30);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const logMiddleware = new LoggingMiddleware();
|
|
440
|
+
DBOS.registerMiddlewareInstaller(logMiddleware);
|
|
441
|
+
|
|
442
|
+
export function logMethodArgs<Args extends unknown[]>(methReg: MethodRegistrationBase, args: Args) {
|
|
443
|
+
// Argument logging
|
|
444
|
+
args.forEach((argValue, idx) => {
|
|
445
|
+
let loggedArgValue = argValue;
|
|
446
|
+
const logMask = getLoggerArgInfo(methReg.args[idx]).logMask;
|
|
447
|
+
|
|
448
|
+
if (logMask === LogMasks.SKIP) {
|
|
449
|
+
return;
|
|
450
|
+
} else {
|
|
451
|
+
if (logMask !== LogMasks.NONE) {
|
|
452
|
+
// For now this means hash
|
|
453
|
+
if (methReg.args[idx].dataType?.dataType === 'json') {
|
|
454
|
+
loggedArgValue = generateSaltedHash(JSON.stringify(argValue), 'JSONSALT');
|
|
455
|
+
} else {
|
|
456
|
+
// Yes, we are doing the same as above for now.
|
|
457
|
+
// It can be better if we have verified the type of the data
|
|
458
|
+
loggedArgValue = generateSaltedHash(JSON.stringify(argValue), 'DBOSSALT');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
DBOS.span?.setAttribute(methReg.args[idx].name, loggedArgValue as string);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return args;
|
|
466
|
+
}
|
|
6
467
|
|
|
7
468
|
export enum APITypes {
|
|
8
469
|
GET = 'GET',
|
|
@@ -37,6 +498,20 @@ export interface DBOSHTTPArgInfo {
|
|
|
37
498
|
argSource?: ArgSources;
|
|
38
499
|
}
|
|
39
500
|
|
|
501
|
+
/**
|
|
502
|
+
* This error can be thrown by DBOS applications to indicate
|
|
503
|
+
* the HTTP response code, in addition to the message.
|
|
504
|
+
* Note that any error with a 'status' field can be used.
|
|
505
|
+
*/
|
|
506
|
+
export class DBOSResponseError extends Error {
|
|
507
|
+
constructor(
|
|
508
|
+
msg: string,
|
|
509
|
+
readonly status: number = 500,
|
|
510
|
+
) {
|
|
511
|
+
super(msg);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
40
515
|
/**
|
|
41
516
|
* HTTPRequest includes useful information from http.IncomingMessage and parsed body,
|
|
42
517
|
* URL parameters, and parsed query string.
|
|
@@ -94,7 +569,7 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
94
569
|
propertyKey: string,
|
|
95
570
|
descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
|
|
96
571
|
) {
|
|
97
|
-
const { regInfo } = DBOS.associateFunctionWithInfo(er, descriptor.value!, {
|
|
572
|
+
const { registration, regInfo } = DBOS.associateFunctionWithInfo(er, descriptor.value!, {
|
|
98
573
|
ctorOrProto: target,
|
|
99
574
|
name: propertyKey,
|
|
100
575
|
});
|
|
@@ -104,6 +579,7 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
104
579
|
apiURL: url,
|
|
105
580
|
apiType: verb,
|
|
106
581
|
});
|
|
582
|
+
requestArgValidation(registration);
|
|
107
583
|
|
|
108
584
|
return descriptor;
|
|
109
585
|
};
|
|
@@ -134,9 +610,27 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
134
610
|
return this.httpApiDec(APITypes.DELETE, url);
|
|
135
611
|
}
|
|
136
612
|
|
|
613
|
+
/** Parameter decorator indicating which source to use (URL, BODY, etc) for arg data */
|
|
614
|
+
static argSource(source: ArgSources) {
|
|
615
|
+
return function (target: object, propertyKey: PropertyKey, param: number) {
|
|
616
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
617
|
+
DBOSHTTP,
|
|
618
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
619
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
620
|
+
{
|
|
621
|
+
ctorOrProto: target,
|
|
622
|
+
name: propertyKey.toString(),
|
|
623
|
+
param,
|
|
624
|
+
},
|
|
625
|
+
) as DBOSHTTPArgInfo;
|
|
626
|
+
|
|
627
|
+
curParam.argSource = source;
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
137
631
|
protected getArgSource(arg: MethodParameter) {
|
|
138
|
-
|
|
139
|
-
return ArgSources.AUTO;
|
|
632
|
+
const arginfo = arg.getRegisteredInfo(DBOSHTTP) as DBOSHTTPArgInfo;
|
|
633
|
+
return arginfo?.argSource ?? ArgSources.AUTO;
|
|
140
634
|
}
|
|
141
635
|
|
|
142
636
|
logRegisteredEndpoints(): void {
|
|
@@ -157,4 +651,29 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
157
651
|
}
|
|
158
652
|
}
|
|
159
653
|
}
|
|
654
|
+
|
|
655
|
+
static argRequired(target: object, propertyKey: PropertyKey, parameterIndex: number) {
|
|
656
|
+
ArgRequired(target, propertyKey, parameterIndex);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
static argOptional(target: object, propertyKey: PropertyKey, parameterIndex: number) {
|
|
660
|
+
ArgOptional(target, propertyKey, parameterIndex);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
static argDate() {
|
|
664
|
+
return ArgDate();
|
|
665
|
+
}
|
|
666
|
+
static argVarchar(n: number) {
|
|
667
|
+
return ArgVarchar(n);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
static defaultArgRequired<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
671
|
+
return DefaultArgRequired(ctor);
|
|
672
|
+
}
|
|
673
|
+
static defaultArgOptional<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
674
|
+
return DefaultArgOptional(ctor);
|
|
675
|
+
}
|
|
676
|
+
static defaultArgValidate<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
677
|
+
return DefaultArgValidate(ctor);
|
|
678
|
+
}
|
|
160
679
|
}
|
package/src/dboskoa.ts
CHANGED
|
@@ -344,7 +344,7 @@ export class DBOSKoa extends DBOSHTTPBase {
|
|
|
344
344
|
} catch (e) {
|
|
345
345
|
if (e instanceof Error) {
|
|
346
346
|
span?.setStatus({ code: SpanStatusCode.ERROR, message: e.message });
|
|
347
|
-
let st = 500;
|
|
347
|
+
let st = (e as { status?: number })?.status || 500;
|
|
348
348
|
if (isClientRequestError(e)) {
|
|
349
349
|
st = 400; // Set to 400: client-side error.
|
|
350
350
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DBOSKoa } from './dboskoa';
|
|
2
|
+
|
|
1
3
|
export {
|
|
2
4
|
ArgSources,
|
|
3
5
|
DBOSHTTP,
|
|
@@ -7,8 +9,23 @@ export {
|
|
|
7
9
|
DBOSHTTPMethodInfo,
|
|
8
10
|
DBOSHTTPReg,
|
|
9
11
|
DBOSHTTPRequest,
|
|
12
|
+
DBOSResponseError,
|
|
10
13
|
RequestIDHeader,
|
|
11
14
|
WorkflowIDHeader,
|
|
12
15
|
} from './dboshttp';
|
|
13
16
|
|
|
14
17
|
export { DBOSKoa, DBOSKoaAuthContext, DBOSKoaClassReg, DBOSKoaAuthMiddleware, DBOSKoaConfig } from './dboskoa';
|
|
18
|
+
|
|
19
|
+
// Export these as unbound functions. We know this is safe,
|
|
20
|
+
// and it more closely matches the existing library syntax.
|
|
21
|
+
// (Using the static function as a decorator, for some reason,
|
|
22
|
+
// is erroneously getting considered as unbound by some lint versions,
|
|
23
|
+
// as there are no parens following it?)
|
|
24
|
+
export const DefaultArgOptional = DBOSKoa.defaultArgOptional;
|
|
25
|
+
export const DefaultArgRequired = DBOSKoa.defaultArgRequired;
|
|
26
|
+
export const DefaultArgValidate = DBOSKoa.defaultArgValidate;
|
|
27
|
+
export const ArgDate = DBOSKoa.argDate;
|
|
28
|
+
export const ArgOptional = DBOSKoa.argOptional;
|
|
29
|
+
export const ArgRequired = DBOSKoa.argRequired;
|
|
30
|
+
export const ArgSource = DBOSKoa.argSource;
|
|
31
|
+
export const ArgVarchar = DBOSKoa.argVarchar;
|