@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.
@@ -2,7 +2,7 @@
2
2
  import Koa from 'koa';
3
3
  import Router from '@koa/router';
4
4
 
5
- import { DBOS, Error as DBOSErrors, StatusString } from '@dbos-inc/dbos-sdk';
5
+ import { DBOS, DBOSResponseError, Error as DBOSErrors, StatusString } from '@dbos-inc/dbos-sdk';
6
6
 
7
7
  import { DBOSKoa, DBOSKoaAuthContext, RequestIDHeader, WorkflowIDHeader } from '../src';
8
8
 
@@ -10,6 +10,11 @@ import request from 'supertest';
10
10
 
11
11
  const dhttp = new DBOSKoa();
12
12
 
13
+ interface TestKvTable {
14
+ id?: number;
15
+ value?: string;
16
+ }
17
+
13
18
  import { randomUUID } from 'node:crypto';
14
19
  import { IncomingMessage } from 'http';
15
20
  import { bodyParser } from '@koa/bodyparser';
@@ -24,9 +29,12 @@ describe('httpserver-tests', () => {
24
29
  let app: Koa;
25
30
  let appRouter: Router;
26
31
 
32
+ const testTableName = 'dbos_test_kv';
33
+
27
34
  beforeAll(async () => {
28
35
  DBOS.setConfig({
29
36
  name: 'dbos-koa-test',
37
+ userDatabaseClient: 'pg-node',
30
38
  });
31
39
  return Promise.resolve();
32
40
  });
@@ -34,6 +42,8 @@ describe('httpserver-tests', () => {
34
42
  beforeEach(async () => {
35
43
  const _classes = [TestEndpoints];
36
44
  await DBOS.launch();
45
+ await DBOS.queryUserDB(`DROP TABLE IF EXISTS ${testTableName};`);
46
+ await DBOS.queryUserDB(`CREATE TABLE IF NOT EXISTS ${testTableName} (id INT PRIMARY KEY, value TEXT);`);
37
47
  app = new Koa();
38
48
  appRouter = new Router();
39
49
  dhttp.registerWithApp(app, appRouter);
@@ -97,12 +107,17 @@ describe('httpserver-tests', () => {
97
107
  });
98
108
 
99
109
  test('post-test-custom-body', async () => {
100
- const response = await request(app.callback())
110
+ let response = await request(app.callback())
101
111
  .post('/testpost')
102
112
  .set('Content-Type', 'application/custom-content-type')
103
113
  .send(JSON.stringify({ name: 'alice' }));
104
114
  expect(response.statusCode).toBe(200);
105
115
  expect(response.text).toBe('hello alice');
116
+ response = await request(app.callback())
117
+ .post('/testpost')
118
+ .set('Content-Type', 'application/rejected-custom-content-type')
119
+ .send(JSON.stringify({ name: 'alice' }));
120
+ expect(response.statusCode).toBe(400);
106
121
  });
107
122
 
108
123
  test('put-test', async () => {
@@ -112,12 +127,17 @@ describe('httpserver-tests', () => {
112
127
  });
113
128
 
114
129
  test('put-test-custom-body', async () => {
115
- const response = await request(app.callback())
130
+ let response = await request(app.callback())
116
131
  .put('/testput')
117
132
  .set('Content-Type', 'application/custom-content-type')
118
133
  .send(JSON.stringify({ name: 'alice' }));
119
134
  expect(response.statusCode).toBe(200);
120
135
  expect(response.text).toBe('hello alice');
136
+ response = await request(app.callback())
137
+ .put('/testput')
138
+ .set('Content-Type', 'application/rejected-custom-content-type')
139
+ .send(JSON.stringify({ name: 'alice' }));
140
+ expect(response.statusCode).toBe(400);
121
141
  });
122
142
 
123
143
  test('patch-test', async () => {
@@ -127,12 +147,23 @@ describe('httpserver-tests', () => {
127
147
  });
128
148
 
129
149
  test('patch-test-custom-body', async () => {
130
- const response = await request(app.callback())
150
+ let response = await request(app.callback())
131
151
  .patch('/testpatch')
132
152
  .set('Content-Type', 'application/custom-content-type')
133
153
  .send(JSON.stringify({ name: 'alice' }));
134
154
  expect(response.statusCode).toBe(200);
135
155
  expect(response.text).toBe('hello alice');
156
+ response = await request(app.callback())
157
+ .patch('/testpatch')
158
+ .set('Content-Type', 'application/rejected-custom-content-type')
159
+ .send(JSON.stringify({ name: 'alice' }));
160
+ expect(response.statusCode).toBe(400);
161
+ });
162
+
163
+ test('endpoint-transaction', async () => {
164
+ const response = await request(app.callback()).post('/transaction/alice');
165
+ expect(response.statusCode).toBe(200);
166
+ expect(response.text).toBe('hello 1');
136
167
  });
137
168
 
138
169
  test('endpoint-step', async () => {
@@ -144,30 +175,31 @@ describe('httpserver-tests', () => {
144
175
  test('endpoint-workflow', async () => {
145
176
  const response = await request(app.callback()).post('/workflow?name=alice');
146
177
  expect(response.statusCode).toBe(200);
147
- expect(response.text).toBe('alice');
178
+ expect(response.text).toBe('hello 1');
148
179
  });
149
180
 
150
181
  test('endpoint-error', async () => {
151
182
  const response = await request(app.callback()).post('/error').send({ name: 'alice' });
152
183
  expect(response.statusCode).toBe(500);
184
+ expect(response.body.details.code).toBe('23505'); // Should be the expected error.
153
185
  });
154
186
 
155
187
  test('endpoint-handler', async () => {
156
188
  const response = await request(app.callback()).get('/handler/alice');
157
189
  expect(response.statusCode).toBe(200);
158
- expect(response.text).toBe('alice');
190
+ expect(response.text).toBe('hello 1');
159
191
  });
160
192
 
161
193
  test('endpoint-testStartWorkflow', async () => {
162
194
  const response = await request(app.callback()).get('/testStartWorkflow/alice');
163
195
  expect(response.statusCode).toBe(200);
164
- expect(response.text).toBe('alice');
196
+ expect(response.text).toBe('hello 1');
165
197
  });
166
198
 
167
199
  test('endpoint-testInvokeWorkflow', async () => {
168
200
  const response = await request(app.callback()).get('/testInvokeWorkflow/alice');
169
201
  expect(response.statusCode).toBe(200);
170
- expect(response.text).toBe('alice');
202
+ expect(response.text).toBe('hello 1');
171
203
  });
172
204
 
173
205
  // This feels unclean, but supertest doesn't expose the error message the people we want. See:
@@ -178,11 +210,17 @@ describe('httpserver-tests', () => {
178
210
 
179
211
  test('response-error', async () => {
180
212
  const response = await request(app.callback()).get('/dbos-error');
181
- expect(response.statusCode).toBe(500);
213
+ expect(response.statusCode).toBe(503);
182
214
  expect((response as unknown as Res).res.statusMessage).toBe('customize error');
183
215
  expect(response.body.message).toBe('customize error');
184
216
  });
185
217
 
218
+ test('datavalidation-error', async () => {
219
+ const response = await request(app.callback()).get('/query');
220
+ expect(response.statusCode).toBe(400);
221
+ expect(response.body.details.dbosErrorCode).toBe(9);
222
+ });
223
+
186
224
  test('dbos-redirect', async () => {
187
225
  const response = await request(app.callback()).get('/redirect');
188
226
  expect(response.statusCode).toBe(302);
@@ -210,17 +248,17 @@ describe('httpserver-tests', () => {
210
248
 
211
249
  test('not-authenticated', async () => {
212
250
  const response = await request(app.callback()).get('/requireduser?name=alice');
213
- expect(response.statusCode).toBe(500);
251
+ expect(response.statusCode).toBe(401);
214
252
  });
215
253
 
216
254
  test('not-you', async () => {
217
255
  const response = await request(app.callback()).get('/requireduser?name=alice&userid=go_away');
218
- expect(response.statusCode).toBe(500);
256
+ expect(response.statusCode).toBe(401);
219
257
  });
220
258
 
221
259
  test('not-authorized', async () => {
222
260
  const response = await request(app.callback()).get('/requireduser?name=alice&userid=bob');
223
- expect(response.statusCode).toBe(500);
261
+ expect(response.statusCode).toBe(403);
224
262
  });
225
263
 
226
264
  test('authorized', async () => {
@@ -230,17 +268,17 @@ describe('httpserver-tests', () => {
230
268
 
231
269
  test('not-authenticated2', async () => {
232
270
  const response = await request(app.callback()).get('/requireduser2?name=alice');
233
- expect(response.statusCode).toBe(500);
271
+ expect(response.statusCode).toBe(401);
234
272
  });
235
273
 
236
274
  test('not-you2', async () => {
237
275
  const response = await request(app.callback()).get('/requireduser2?name=alice&userid=go_away');
238
- expect(response.statusCode).toBe(500);
276
+ expect(response.statusCode).toBe(401);
239
277
  });
240
278
 
241
279
  test('not-authorized2', async () => {
242
280
  const response = await request(app.callback()).get('/requireduser2?name=alice&userid=bob');
243
- expect(response.statusCode).toBe(500);
281
+ expect(response.statusCode).toBe(403);
244
282
  });
245
283
 
246
284
  test('authorized2', async () => {
@@ -254,12 +292,12 @@ describe('httpserver-tests', () => {
254
292
  .post('/workflow?name=bob')
255
293
  .set({ 'dbos-idempotency-key': workflowID });
256
294
  expect(response.statusCode).toBe(200);
257
- expect(response.text).toBe('bob');
295
+ expect(response.text).toBe('hello 1');
258
296
 
259
297
  // Retrieve the workflow with WFID.
260
298
  const retrievedHandle = DBOS.retrieveWorkflow(workflowID);
261
299
  expect(retrievedHandle).not.toBeNull();
262
- await expect(retrievedHandle.getResult()).resolves.toBe('bob');
300
+ await expect(retrievedHandle.getResult()).resolves.toBe('hello 1');
263
301
  await expect(retrievedHandle.getStatus()).resolves.toMatchObject({
264
302
  status: StatusString.SUCCESS,
265
303
  });
@@ -269,12 +307,12 @@ describe('httpserver-tests', () => {
269
307
  const workflowID = randomUUID();
270
308
  const response = await request(app.callback()).get('/handler/bob').set({ 'dbos-idempotency-key': workflowID });
271
309
  expect(response.statusCode).toBe(200);
272
- expect(response.text).toBe('bob');
310
+ expect(response.text).toBe('hello 1');
273
311
 
274
312
  // Retrieve the workflow with WFID.
275
313
  const retrievedHandle = DBOS.retrieveWorkflow(workflowID);
276
314
  expect(retrievedHandle).not.toBeNull();
277
- await expect(retrievedHandle.getResult()).resolves.toBe('bob');
315
+ await expect(retrievedHandle.getResult()).resolves.toBe('hello 1');
278
316
  await expect(retrievedHandle.getStatus()).resolves.toMatchObject({
279
317
  status: StatusString.SUCCESS,
280
318
  });
@@ -311,6 +349,7 @@ describe('httpserver-tests', () => {
311
349
  parsedMethods: ['POST', 'PUT', 'PATCH', 'GET', 'DELETE'],
312
350
  }),
313
351
  )
352
+ @DBOSKoa.defaultArgRequired
314
353
  class TestEndpoints {
315
354
  @dhttp.getApi('/hello')
316
355
  static async hello() {
@@ -339,12 +378,6 @@ describe('httpserver-tests', () => {
339
378
  return Promise.resolve(url);
340
379
  }
341
380
 
342
- @dhttp.getApi('/dbos-error')
343
- @DBOS.workflow()
344
- static async dbosErr() {
345
- return Promise.reject(new Error('customize error'));
346
- }
347
-
348
381
  @dhttp.getApi('/query')
349
382
  static async helloQuery(name: string) {
350
383
  DBOS.logger.info(`query with name ${name}`); // Test logging.
@@ -390,6 +423,12 @@ describe('httpserver-tests', () => {
390
423
  return Promise.resolve(`hello ${name}`);
391
424
  }
392
425
 
426
+ @dhttp.getApi('/dbos-error')
427
+ @DBOS.transaction()
428
+ static async dbosErr() {
429
+ return Promise.reject(new DBOSResponseError('customize error', 503));
430
+ }
431
+
393
432
  @dhttp.getApi('/handler/:name')
394
433
  static async testHandler(name: string) {
395
434
  const workflowID: string = DBOSKoa.koaContext.get(WorkflowIDHeader);
@@ -411,6 +450,16 @@ describe('httpserver-tests', () => {
411
450
  return await TestEndpoints.testWorkflow(name);
412
451
  }
413
452
 
453
+ @dhttp.postApi('/transaction/:name')
454
+ @DBOS.transaction()
455
+ static async testTransaction(name: string) {
456
+ const { rows } = await DBOS.pgClient.query<TestKvTable>(
457
+ `INSERT INTO ${testTableName}(id, value) VALUES (1, $1) RETURNING id`,
458
+ [name],
459
+ );
460
+ return `hello ${rows[0].id}`;
461
+ }
462
+
414
463
  @dhttp.getApi('/step/:input')
415
464
  @DBOS.step()
416
465
  static async testStep(input: string) {
@@ -420,27 +469,30 @@ describe('httpserver-tests', () => {
420
469
  @dhttp.postApi('/workflow')
421
470
  @DBOS.workflow()
422
471
  static async testWorkflow(name: string) {
423
- return TestEndpoints.testStep(name);
472
+ const res = await TestEndpoints.testTransaction(name);
473
+ return TestEndpoints.testStep(res);
424
474
  }
425
475
 
426
476
  @dhttp.postApi('/error')
427
477
  @DBOS.workflow()
428
478
  static async testWorkflowError(name: string) {
429
- await Promise.resolve();
430
- throw Error(name);
479
+ // This workflow should encounter duplicate primary key error.
480
+ let res = await TestEndpoints.testTransaction(name);
481
+ res = await TestEndpoints.testTransaction(name);
482
+ return res;
431
483
  }
432
484
 
433
485
  @dhttp.getApi('/requireduser')
434
486
  @DBOS.requiredRole(['user'])
435
487
  static async testAuth(name: string) {
436
488
  if (DBOS.authenticatedUser !== 'a_real_user') {
437
- throw new Error('uid not a real user!');
489
+ throw new DBOSResponseError('uid not a real user!', 400);
438
490
  }
439
491
  if (!DBOS.authenticatedRoles.includes('user')) {
440
- throw new Error("roles don't include user!");
492
+ throw new DBOSResponseError("roles don't include user!", 400);
441
493
  }
442
494
  if (DBOS.assumedRole !== 'user') {
443
- throw new Error('Should never happen! Not assumed to be user');
495
+ throw new DBOSResponseError('Should never happen! Not assumed to be user', 400);
444
496
  }
445
497
  return Promise.resolve(`Please say hello to ${name}`);
446
498
  }
@@ -449,13 +501,13 @@ describe('httpserver-tests', () => {
449
501
  @DBOS.requiredRole(['user'])
450
502
  static async testAuth2(name: string) {
451
503
  if (DBOS.authenticatedUser !== 'a_real_user') {
452
- throw new Error('uid not a real user!');
504
+ throw new DBOSResponseError('uid not a real user!', 400);
453
505
  }
454
506
  if (!DBOS.authenticatedRoles.includes('user')) {
455
- throw new Error("roles don't include user!");
507
+ throw new DBOSResponseError("roles don't include user!", 400);
456
508
  }
457
509
  if (DBOS.assumedRole !== 'user') {
458
- throw new Error('Should never happen! Not assumed to be user');
510
+ throw new DBOSResponseError('Should never happen! Not assumed to be user', 400);
459
511
  }
460
512
  return Promise.resolve(`Please say hello to ${name}`);
461
513
  }
@@ -56,9 +56,14 @@ describe('registerstep', () => {
56
56
  expect(response1.statusCode).toBe(200);
57
57
  const response2 = await request(app.callback()).get('/api/i2?user=jeremy');
58
58
  expect(response2.statusCode).toBe(200);
59
+ const response3 = await request(app.callback()).get('/api/i1');
60
+ expect(response3.statusCode).toBe(400);
61
+ const response4 = await request(app.callback()).get('/api/i2');
62
+ expect(response4.statusCode).toBe(400);
59
63
  });
60
64
  });
61
65
 
66
+ @DBOSKoa.defaultArgValidate
62
67
  class KnexKoa {
63
68
  @customstep()
64
69
  @dhttp.getApi('/api/i2')
@@ -77,9 +77,14 @@ describe('KnexDataSource', () => {
77
77
  expect(response1.statusCode).toBe(200);
78
78
  const response2 = await request(app.callback()).get('/api/i2?user=jeremy');
79
79
  expect(response2.statusCode).toBe(200);
80
+ const response3 = await request(app.callback()).get('/api/i1');
81
+ expect(response3.statusCode).toBe(400);
82
+ const response4 = await request(app.callback()).get('/api/i2');
83
+ expect(response4.statusCode).toBe(400);
80
84
  });
81
85
  });
82
86
 
87
+ @DBOSKoa.defaultArgValidate
83
88
  class KnexKoa {
84
89
  @knexds.transaction()
85
90
  @dhttp.getApi('/api/i2')