@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbos-inc/koa-serve",
3
- "version": "3.5.44-preview.gc094fdab44",
3
+ "version": "3.6.3-preview",
4
4
  "description": "DBOS HTTP Package for serving workflows with Koa",
5
5
  "license": "MIT",
6
6
  "repository": {
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 { DBOS, DBOSLifecycleCallback, Error as DBOSErrors, MethodParameter } from '@dbos-inc/dbos-sdk';
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
- void arg;
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
+ });
@@ -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(500);
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(500);
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(500);
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.step()
149
- static async testStep(name: string) {
150
- return Promise.resolve(`hello ${name}`);
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.testStep(name);
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 {