@dbos-inc/koa-serve 3.5.44-preview.gc094fdab44 → 3.6.3-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 +15 -0
- package/dist/src/dboshttp.d.ts.map +1 -1
- package/dist/src/dboshttp.js +38 -3
- 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 +8 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +17 -3
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/dboshttp.ts +61 -4
- package/src/dboskoa.ts +1 -1
- package/src/index.ts +16 -0
- package/tests/argsource.test.ts +151 -0
- package/tests/auth.test.ts +42 -7
- package/tests/endpoints.test.ts +86 -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
package/src/dboshttp.ts
CHANGED
|
@@ -2,7 +2,20 @@ import { IncomingHttpHeaders } from 'http';
|
|
|
2
2
|
import { ParsedUrlQuery } from 'querystring';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DBOS,
|
|
7
|
+
DBOSLifecycleCallback,
|
|
8
|
+
Error as DBOSErrors,
|
|
9
|
+
MethodParameter,
|
|
10
|
+
requestArgValidation,
|
|
11
|
+
ArgRequired,
|
|
12
|
+
ArgOptional,
|
|
13
|
+
DefaultArgRequired,
|
|
14
|
+
DefaultArgValidate,
|
|
15
|
+
DefaultArgOptional,
|
|
16
|
+
ArgDate,
|
|
17
|
+
ArgVarchar,
|
|
18
|
+
} from '@dbos-inc/dbos-sdk';
|
|
6
19
|
|
|
7
20
|
export enum APITypes {
|
|
8
21
|
GET = 'GET',
|
|
@@ -94,7 +107,7 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
94
107
|
propertyKey: string,
|
|
95
108
|
descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
|
|
96
109
|
) {
|
|
97
|
-
const { regInfo } = DBOS.associateFunctionWithInfo(er, descriptor.value!, {
|
|
110
|
+
const { registration, regInfo } = DBOS.associateFunctionWithInfo(er, descriptor.value!, {
|
|
98
111
|
ctorOrProto: target,
|
|
99
112
|
name: propertyKey,
|
|
100
113
|
});
|
|
@@ -104,6 +117,7 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
104
117
|
apiURL: url,
|
|
105
118
|
apiType: verb,
|
|
106
119
|
});
|
|
120
|
+
requestArgValidation(registration);
|
|
107
121
|
|
|
108
122
|
return descriptor;
|
|
109
123
|
};
|
|
@@ -134,9 +148,27 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
134
148
|
return this.httpApiDec(APITypes.DELETE, url);
|
|
135
149
|
}
|
|
136
150
|
|
|
151
|
+
/** Parameter decorator indicating which source to use (URL, BODY, etc) for arg data */
|
|
152
|
+
static argSource(source: ArgSources) {
|
|
153
|
+
return function (target: object, propertyKey: PropertyKey, parameterIndex: number) {
|
|
154
|
+
const curParam = DBOS.associateParamWithInfo(
|
|
155
|
+
DBOSHTTP,
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
157
|
+
Object.getOwnPropertyDescriptor(target, propertyKey)!.value,
|
|
158
|
+
{
|
|
159
|
+
ctorOrProto: target,
|
|
160
|
+
name: propertyKey.toString(),
|
|
161
|
+
param: parameterIndex,
|
|
162
|
+
},
|
|
163
|
+
) as DBOSHTTPArgInfo;
|
|
164
|
+
|
|
165
|
+
curParam.argSource = source;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
137
169
|
protected getArgSource(arg: MethodParameter) {
|
|
138
|
-
|
|
139
|
-
return ArgSources.AUTO;
|
|
170
|
+
const arginfo = arg.getRegisteredInfo(DBOSHTTP) as DBOSHTTPArgInfo;
|
|
171
|
+
return arginfo?.argSource ?? ArgSources.AUTO;
|
|
140
172
|
}
|
|
141
173
|
|
|
142
174
|
logRegisteredEndpoints(): void {
|
|
@@ -157,4 +189,29 @@ export class DBOSHTTPBase implements DBOSLifecycleCallback {
|
|
|
157
189
|
}
|
|
158
190
|
}
|
|
159
191
|
}
|
|
192
|
+
|
|
193
|
+
static argRequired(target: object, propertyKey: PropertyKey, parameterIndex: number) {
|
|
194
|
+
ArgRequired(target, propertyKey, parameterIndex);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static argOptional(target: object, propertyKey: PropertyKey, parameterIndex: number) {
|
|
198
|
+
ArgOptional(target, propertyKey, parameterIndex);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static argDate() {
|
|
202
|
+
return ArgDate();
|
|
203
|
+
}
|
|
204
|
+
static argVarchar(n: number) {
|
|
205
|
+
return ArgVarchar(n);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static defaultArgRequired<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
209
|
+
return DefaultArgRequired(ctor);
|
|
210
|
+
}
|
|
211
|
+
static defaultArgOptional<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
212
|
+
return DefaultArgOptional(ctor);
|
|
213
|
+
}
|
|
214
|
+
static defaultArgValidate<T extends { new (...args: unknown[]): object }>(ctor: T) {
|
|
215
|
+
return DefaultArgValidate(ctor);
|
|
216
|
+
}
|
|
160
217
|
}
|
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 DBOSErrors.DBOSResponseError)?.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,
|
|
@@ -12,3 +14,17 @@ export {
|
|
|
12
14
|
} from './dboshttp';
|
|
13
15
|
|
|
14
16
|
export { DBOSKoa, DBOSKoaAuthContext, DBOSKoaClassReg, DBOSKoaAuthMiddleware, DBOSKoaConfig } from './dboskoa';
|
|
17
|
+
|
|
18
|
+
// Export these as unbound functions. We know this is safe,
|
|
19
|
+
// and it more closely matches the existing library syntax.
|
|
20
|
+
// (Using the static function as a decorator, for some reason,
|
|
21
|
+
// is erroneously getting considered as unbound by some lint versions,
|
|
22
|
+
// as there are no parens following it?)
|
|
23
|
+
export const DefaultArgOptional = DBOSKoa.defaultArgOptional;
|
|
24
|
+
export const DefaultArgRequired = DBOSKoa.defaultArgRequired;
|
|
25
|
+
export const DefaultArgValidate = DBOSKoa.defaultArgValidate;
|
|
26
|
+
export const ArgDate = DBOSKoa.argDate;
|
|
27
|
+
export const ArgOptional = DBOSKoa.argOptional;
|
|
28
|
+
export const ArgRequired = DBOSKoa.argRequired;
|
|
29
|
+
export const ArgSource = DBOSKoa.argSource;
|
|
30
|
+
export const ArgVarchar = DBOSKoa.argVarchar;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import Koa from 'koa';
|
|
2
|
+
import Router from '@koa/router';
|
|
3
|
+
|
|
4
|
+
import { DBOS } from '@dbos-inc/dbos-sdk';
|
|
5
|
+
|
|
6
|
+
import { ArgSources, DBOSKoa } from '../src';
|
|
7
|
+
|
|
8
|
+
import request from 'supertest';
|
|
9
|
+
import bodyParser from '@koa/bodyparser';
|
|
10
|
+
|
|
11
|
+
const dhttp = new DBOSKoa();
|
|
12
|
+
|
|
13
|
+
describe('httpserver-argsource-tests', () => {
|
|
14
|
+
let app: Koa;
|
|
15
|
+
let appRouter: Router;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
DBOS.setConfig({
|
|
19
|
+
name: 'dbos-koa-test',
|
|
20
|
+
userDatabaseClient: 'pg-node',
|
|
21
|
+
});
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
const _classes = [ArgTestEndpoints];
|
|
27
|
+
await DBOS.launch();
|
|
28
|
+
app = new Koa();
|
|
29
|
+
appRouter = new Router();
|
|
30
|
+
dhttp.registerWithApp(app, appRouter);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await DBOS.shutdown();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('get-query', async () => {
|
|
38
|
+
const response1 = await request(app.callback()).get('/getquery?name=alice');
|
|
39
|
+
expect(response1.statusCode).toBe(200);
|
|
40
|
+
expect(response1.text).toBe('hello alice');
|
|
41
|
+
const response2 = await request(app.callback()).get('/getquery').send({ name: 'alice' });
|
|
42
|
+
expect(response2.statusCode).toBe(400);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('get-body', async () => {
|
|
46
|
+
const response1 = await request(app.callback()).get('/getbody?name=alice');
|
|
47
|
+
expect(response1.statusCode).toBe(400);
|
|
48
|
+
const response2 = await request(app.callback()).get('/getbody').send({ name: 'alice' });
|
|
49
|
+
expect(response2.statusCode).toBe(200);
|
|
50
|
+
expect(response2.text).toBe('hello alice');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('get-default', async () => {
|
|
54
|
+
const response1 = await request(app.callback()).get('/getdefault?name=alice');
|
|
55
|
+
expect(response1.statusCode).toBe(200);
|
|
56
|
+
expect(response1.text).toBe('hello alice');
|
|
57
|
+
const response2 = await request(app.callback()).get('/getdefault').send({ name: 'alice' });
|
|
58
|
+
expect(response2.statusCode).toBe(400);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('get-auto', async () => {
|
|
62
|
+
const response1 = await request(app.callback()).get('/getauto?name=alice');
|
|
63
|
+
expect(response1.statusCode).toBe(200);
|
|
64
|
+
expect(response1.text).toBe('hello alice');
|
|
65
|
+
const response2 = await request(app.callback()).get('/getauto').send({ name: 'alice' });
|
|
66
|
+
expect(response2.statusCode).toBe(200);
|
|
67
|
+
expect(response2.text).toBe('hello alice');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('post-query', async () => {
|
|
71
|
+
const response1 = await request(app.callback()).post('/postquery?name=alice');
|
|
72
|
+
expect(response1.statusCode).toBe(200);
|
|
73
|
+
expect(response1.text).toBe('hello alice');
|
|
74
|
+
const response2 = await request(app.callback()).post('/postquery').send({ name: 'alice' });
|
|
75
|
+
expect(response2.statusCode).toBe(400);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('post-body', async () => {
|
|
79
|
+
const response1 = await request(app.callback()).post('/postbody?name=alice');
|
|
80
|
+
expect(response1.statusCode).toBe(400);
|
|
81
|
+
const response2 = await request(app.callback()).post('/postbody').send({ name: 'alice' });
|
|
82
|
+
expect(response2.statusCode).toBe(200);
|
|
83
|
+
expect(response2.text).toBe('hello alice');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('post-default', async () => {
|
|
87
|
+
const response1 = await request(app.callback()).post('/postdefault?name=alice');
|
|
88
|
+
expect(response1.statusCode).toBe(400);
|
|
89
|
+
const response2 = await request(app.callback()).post('/postdefault').send({ name: 'alice' });
|
|
90
|
+
expect(response2.statusCode).toBe(200);
|
|
91
|
+
expect(response2.text).toBe('hello alice');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('post-auto', async () => {
|
|
95
|
+
const response1 = await request(app.callback()).post('/postauto?name=alice');
|
|
96
|
+
expect(response1.statusCode).toBe(200);
|
|
97
|
+
expect(response1.text).toBe('hello alice');
|
|
98
|
+
const response2 = await request(app.callback()).post('/postauto').send({ name: 'alice' });
|
|
99
|
+
expect(response2.statusCode).toBe(200);
|
|
100
|
+
expect(response2.text).toBe('hello alice');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
@dhttp.koaBodyParser(
|
|
104
|
+
bodyParser({
|
|
105
|
+
enableTypes: ['json'],
|
|
106
|
+
parsedMethods: ['GET', 'POST'],
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
@DBOSKoa.defaultArgRequired
|
|
110
|
+
class ArgTestEndpoints {
|
|
111
|
+
@dhttp.getApi('/getquery')
|
|
112
|
+
static async getQuery(@DBOSKoa.argSource(ArgSources.QUERY) name: string) {
|
|
113
|
+
return Promise.resolve(`hello ${name}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@dhttp.getApi('/getbody')
|
|
117
|
+
static async getBody(@DBOSKoa.argSource(ArgSources.BODY) name: string) {
|
|
118
|
+
return Promise.resolve(`hello ${name}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@dhttp.getApi('/getdefault')
|
|
122
|
+
static async getDefault(@DBOSKoa.argSource(ArgSources.DEFAULT) name: string) {
|
|
123
|
+
return Promise.resolve(`hello ${name}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@dhttp.getApi('/getauto')
|
|
127
|
+
static async getAuto(@DBOSKoa.argSource(ArgSources.AUTO) name: string) {
|
|
128
|
+
return Promise.resolve(`hello ${name}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@dhttp.postApi('/postquery')
|
|
132
|
+
static async postQuery(@DBOSKoa.argSource(ArgSources.QUERY) name: string) {
|
|
133
|
+
return Promise.resolve(`hello ${name}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@dhttp.postApi('/postbody')
|
|
137
|
+
static async postBody(@DBOSKoa.argSource(ArgSources.BODY) name: string) {
|
|
138
|
+
return Promise.resolve(`hello ${name}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@dhttp.postApi('/postdefault')
|
|
142
|
+
static async postDefault(@DBOSKoa.argSource(ArgSources.DEFAULT) name: string) {
|
|
143
|
+
return Promise.resolve(`hello ${name}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@dhttp.postApi('/postauto')
|
|
147
|
+
static async postAuto(@DBOSKoa.argSource(ArgSources.AUTO) name: string) {
|
|
148
|
+
return Promise.resolve(`hello ${name}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
package/tests/auth.test.ts
CHANGED
|
@@ -9,13 +9,21 @@ import request from 'supertest';
|
|
|
9
9
|
|
|
10
10
|
const dhttp = new DBOSKoa();
|
|
11
11
|
|
|
12
|
+
interface TestKvTable {
|
|
13
|
+
id?: number;
|
|
14
|
+
value?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
describe('httpserver-defsec-tests', () => {
|
|
13
18
|
let app: Koa;
|
|
14
19
|
let appRouter: Router;
|
|
15
20
|
|
|
21
|
+
const testTableName = 'dbos_test_kv';
|
|
22
|
+
|
|
16
23
|
beforeAll(async () => {
|
|
17
24
|
DBOS.setConfig({
|
|
18
25
|
name: 'dbos-koa-test',
|
|
26
|
+
userDatabaseClient: 'pg-node',
|
|
19
27
|
});
|
|
20
28
|
return Promise.resolve();
|
|
21
29
|
});
|
|
@@ -23,6 +31,8 @@ describe('httpserver-defsec-tests', () => {
|
|
|
23
31
|
beforeEach(async () => {
|
|
24
32
|
const _classes = [TestEndpointDefSec, SecondClass];
|
|
25
33
|
await DBOS.launch();
|
|
34
|
+
await DBOS.queryUserDB(`DROP TABLE IF EXISTS ${testTableName};`);
|
|
35
|
+
await DBOS.queryUserDB(`CREATE TABLE IF NOT EXISTS ${testTableName} (id SERIAL PRIMARY KEY, value TEXT);`);
|
|
26
36
|
middlewareCounter = 0;
|
|
27
37
|
middlewareCounter2 = 0;
|
|
28
38
|
middlewareCounterG = 0;
|
|
@@ -61,17 +71,17 @@ describe('httpserver-defsec-tests', () => {
|
|
|
61
71
|
|
|
62
72
|
test('not-authenticated', async () => {
|
|
63
73
|
const response = await request(app.callback()).get('/requireduser?name=alice');
|
|
64
|
-
expect(response.statusCode).toBe(
|
|
74
|
+
expect(response.statusCode).toBe(401);
|
|
65
75
|
});
|
|
66
76
|
|
|
67
77
|
test('not-you', async () => {
|
|
68
78
|
const response = await request(app.callback()).get('/requireduser?name=alice&userid=go_away');
|
|
69
|
-
expect(response.statusCode).toBe(
|
|
79
|
+
expect(response.statusCode).toBe(401);
|
|
70
80
|
});
|
|
71
81
|
|
|
72
82
|
test('not-authorized', async () => {
|
|
73
83
|
const response = await request(app.callback()).get('/requireduser?name=alice&userid=bob');
|
|
74
|
-
expect(response.statusCode).toBe(
|
|
84
|
+
expect(response.statusCode).toBe(403);
|
|
75
85
|
});
|
|
76
86
|
|
|
77
87
|
test('authorized', async () => {
|
|
@@ -83,6 +93,22 @@ describe('httpserver-defsec-tests', () => {
|
|
|
83
93
|
test('cascade-authorized', async () => {
|
|
84
94
|
const response = await request(app.callback()).get('/workflow?name=alice&userid=a_real_user');
|
|
85
95
|
expect(response.statusCode).toBe(200);
|
|
96
|
+
|
|
97
|
+
const txnResponse = await request(app.callback()).get('/transaction?name=alice&userid=a_real_user');
|
|
98
|
+
expect(txnResponse.statusCode).toBe(200);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// We can directly test a transaction with passed in authorizedRoles.
|
|
102
|
+
test('direct-transaction-test', async () => {
|
|
103
|
+
await DBOS.withAuthedContext('user', ['user'], async () => {
|
|
104
|
+
const res = await TestEndpointDefSec.testTranscation('alice');
|
|
105
|
+
expect(res).toBe('hello 1');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Unauthorized.
|
|
109
|
+
await expect(TestEndpointDefSec.testTranscation('alice')).rejects.toThrow(
|
|
110
|
+
new DBOSError.DBOSNotAuthorizedError('User does not have a role with permission to call testTranscation', 403),
|
|
111
|
+
);
|
|
86
112
|
});
|
|
87
113
|
|
|
88
114
|
async function authTestMiddleware(ctx: DBOSKoaAuthContext) {
|
|
@@ -145,14 +171,18 @@ describe('httpserver-defsec-tests', () => {
|
|
|
145
171
|
return Promise.resolve(`Please say hello to ${name}`);
|
|
146
172
|
}
|
|
147
173
|
|
|
148
|
-
@DBOS.
|
|
149
|
-
static async
|
|
150
|
-
|
|
174
|
+
@DBOS.transaction()
|
|
175
|
+
static async testTranscation(name: string) {
|
|
176
|
+
const { rows } = await DBOS.pgClient.query<TestKvTable>(
|
|
177
|
+
`INSERT INTO ${testTableName}(value) VALUES ($1) RETURNING id`,
|
|
178
|
+
[name],
|
|
179
|
+
);
|
|
180
|
+
return `hello ${rows[0].id}`;
|
|
151
181
|
}
|
|
152
182
|
|
|
153
183
|
@DBOS.workflow()
|
|
154
184
|
static async testWorkflow(name: string) {
|
|
155
|
-
const res = await TestEndpointDefSec.
|
|
185
|
+
const res = await TestEndpointDefSec.testTranscation(name);
|
|
156
186
|
return res;
|
|
157
187
|
}
|
|
158
188
|
|
|
@@ -160,6 +190,11 @@ describe('httpserver-defsec-tests', () => {
|
|
|
160
190
|
static async testWfEndpoint(name: string) {
|
|
161
191
|
return await TestEndpointDefSec.testWorkflow(name);
|
|
162
192
|
}
|
|
193
|
+
|
|
194
|
+
@dhttp.getApi('/transaction')
|
|
195
|
+
static async testTxnEndpoint(name: string) {
|
|
196
|
+
return await TestEndpointDefSec.testTranscation(name);
|
|
197
|
+
}
|
|
163
198
|
}
|
|
164
199
|
|
|
165
200
|
class SecondClass {
|