@appsemble/node-utils 0.21.2 → 0.22.0

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.
Files changed (37) hide show
  1. package/EmailQuotaExceededError.d.ts +3 -0
  2. package/EmailQuotaExceededError.js +8 -0
  3. package/README.md +3 -3
  4. package/index.d.ts +3 -1
  5. package/index.js +3 -1
  6. package/jsonschema.d.ts +2 -1
  7. package/jsonschema.js +14 -3
  8. package/middleware/error.d.ts +2 -0
  9. package/middleware/error.js +27 -0
  10. package/middleware/{boom.test.js → error.test.js} +23 -12
  11. package/package.json +10 -7
  12. package/resource.d.ts +2 -1
  13. package/resource.js +39 -6
  14. package/server/controllers/action.js +61 -11
  15. package/server/controllers/appMembers.js +21 -4
  16. package/server/controllers/appMessages.js +20 -4
  17. package/server/controllers/assets.js +28 -5
  18. package/server/controllers/oauth2Provider.js +1 -1
  19. package/server/controllers/resources.js +46 -11
  20. package/server/controllers/resources.test.js +2 -2
  21. package/server/controllers/teams.js +7 -2
  22. package/server/routes/appRouter/blockAssetHandler.js +7 -2
  23. package/server/routes/appRouter/blockCssHandler.js +7 -2
  24. package/server/routes/appRouter/bulmaHandler.js +2 -3
  25. package/server/routes/appRouter/cssHandler.js +7 -2
  26. package/server/routes/appRouter/index.js +2 -3
  27. package/server/routes/appRouter/indexHandler.js +3 -4
  28. package/server/routes/appRouter/manifestHandler.js +7 -2
  29. package/server/routes/appRouter/screenshotHandler.js +14 -3
  30. package/server/routes/appRouter/serviceWorkerHandler.js +9 -1
  31. package/server/types.d.ts +4 -1
  32. package/tinyRouter.js +7 -2
  33. package/tinyRouter.test.js +3 -6
  34. package/middleware/boom.d.ts +0 -7
  35. package/middleware/boom.js +0 -32
  36. package/vitest.config.d.ts +0 -2
  37. /package/middleware/{boom.test.d.ts → error.test.d.ts} +0 -0
@@ -0,0 +1,3 @@
1
+ export declare class EmailQuotaExceededError extends Error {
2
+ constructor(message: string);
3
+ }
@@ -0,0 +1,8 @@
1
+ export class EmailQuotaExceededError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'EmailQuotaExceededError';
5
+ Error.captureStackTrace(this, EmailQuotaExceededError);
6
+ }
7
+ }
8
+ //# sourceMappingURL=EmailQuotaExceededError.js.map
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.21.2/config/assets/logo.svg) Appsemble Node Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.22.0/config/assets/logo.svg) Appsemble Node Utilities
2
2
 
3
3
  > NodeJS utilities used by Appsemble internally.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@appsemble/node-utils)](https://www.npmjs.com/package/@appsemble/node-utils)
6
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.21.2/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.21.2)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.22.0/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.22.0)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
8
8
 
9
9
  ## Table of Contents
@@ -26,5 +26,5 @@ compatibility is not guaranteed.
26
26
 
27
27
  ## License
28
28
 
29
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.21.2/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.22.0/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
package/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export * from './handleError.js';
7
7
  export * from './interceptors.js';
8
8
  export * from './logger.js';
9
9
  export * from './middleware/loggerMiddleware.js';
10
- export * from './middleware/boom.js';
10
+ export * from './middleware/error.js';
11
11
  export * from './middleware/conditional.js';
12
12
  export * from './middleware/frontend.js';
13
13
  export * from './parsers.js';
@@ -30,3 +30,5 @@ export * from './server/routes/appRouter/iconHandler.js';
30
30
  export * from './server/controllers/index.js';
31
31
  export * from './server/types.js';
32
32
  export * from './koa.js';
33
+ export { bulmaVersion, faVersion, version } from './versions.cjs';
34
+ export * from './EmailQuotaExceededError.js';
package/index.js CHANGED
@@ -7,7 +7,7 @@ export * from './handleError.js';
7
7
  export * from './interceptors.js';
8
8
  export * from './logger.js';
9
9
  export * from './middleware/loggerMiddleware.js';
10
- export * from './middleware/boom.js';
10
+ export * from './middleware/error.js';
11
11
  export * from './middleware/conditional.js';
12
12
  export * from './middleware/frontend.js';
13
13
  export * from './parsers.js';
@@ -30,4 +30,6 @@ export * from './server/routes/appRouter/iconHandler.js';
30
30
  export * from './server/controllers/index.js';
31
31
  export * from './server/types.js';
32
32
  export * from './koa.js';
33
+ export { bulmaVersion, faVersion, version } from './versions.cjs';
34
+ export * from './EmailQuotaExceededError.js';
33
35
  //# sourceMappingURL=index.js.map
package/jsonschema.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import { type ValidatorResult } from 'jsonschema';
2
- export declare function handleValidatorResult(result: ValidatorResult, msg?: string): void;
2
+ import { type Context } from 'koa';
3
+ export declare function handleValidatorResult(ctx: Context, result: ValidatorResult, msg?: string): void;
package/jsonschema.js CHANGED
@@ -1,7 +1,18 @@
1
- import { badRequest } from '@hapi/boom';
2
- export function handleValidatorResult(result, msg = 'JSON schema validation failed') {
1
+ export function handleValidatorResult(ctx, result, msg = 'JSON schema validation failed') {
3
2
  if (!result.valid) {
4
- throw badRequest(msg, { errors: result.errors });
3
+ if (ctx.response === undefined) {
4
+ throw new Error(msg, { cause: result.errors });
5
+ }
6
+ ctx.response.status = 400;
7
+ ctx.response.body = {
8
+ statusCode: 400,
9
+ error: 'Bad Request',
10
+ message: msg,
11
+ data: {
12
+ errors: result.errors,
13
+ },
14
+ };
15
+ ctx.throw();
5
16
  }
6
17
  }
7
18
  //# sourceMappingURL=jsonschema.js.map
@@ -0,0 +1,2 @@
1
+ import { type Middleware } from 'koa';
2
+ export declare function errorMiddleware(): Middleware;
@@ -0,0 +1,27 @@
1
+ export function errorMiddleware() {
2
+ return async (ctx, next) => {
3
+ try {
4
+ await next();
5
+ }
6
+ catch (error) {
7
+ ctx.response.type = 'json';
8
+ if (ctx.status && ctx.body) {
9
+ ctx.app.emit('event', error, ctx);
10
+ }
11
+ else if (error instanceof Error &&
12
+ error.message === 'Fields "subject" and "body" must be a valid string') {
13
+ ctx.response.status = 400;
14
+ ctx.response.body = {
15
+ error: 'Bad Request',
16
+ message: 'Fields "subject" and "body" must be a valid string',
17
+ statusCode: 400,
18
+ };
19
+ ctx.app.emit('event', error, ctx);
20
+ }
21
+ else {
22
+ throw error;
23
+ }
24
+ }
25
+ };
26
+ }
27
+ //# sourceMappingURL=error.js.map
@@ -1,17 +1,22 @@
1
- import { notFound, unauthorized } from '@hapi/boom';
2
1
  import { request, setTestApp } from 'axios-test-instance';
3
2
  import Koa from 'koa';
4
- import { boomMiddleware } from './boom.js';
5
- describe('boomMiddleware', () => {
3
+ import { errorMiddleware } from './error.js';
4
+ describe('errorMiddleware', () => {
6
5
  let app;
7
6
  beforeEach(async () => {
8
7
  app = new Koa();
9
- app.use(boomMiddleware());
8
+ app.use(errorMiddleware());
10
9
  await setTestApp(app);
11
10
  });
12
- it('should catch boom errors', async () => {
13
- app.use(() => {
14
- throw notFound('Error not found', "It's nowhere to be seen!");
11
+ it('should catch Koa errors', async () => {
12
+ app.use((ctx) => {
13
+ ctx.response.status = 404;
14
+ ctx.response.body = {
15
+ statusCode: 404,
16
+ error: 'Not Found',
17
+ message: 'Error not found',
18
+ };
19
+ ctx.throw();
15
20
  });
16
21
  const response = await request.get('/');
17
22
  expect(response).toMatchObject({
@@ -20,11 +25,10 @@ describe('boomMiddleware', () => {
20
25
  statusCode: 404,
21
26
  error: 'Not Found',
22
27
  message: 'Error not found',
23
- data: "It's nowhere to be seen!",
24
28
  },
25
29
  });
26
30
  });
27
- it('should rethrow non-boom errors', async () => {
31
+ it('should rethrow non-Koa errors', async () => {
28
32
  app.silent = true;
29
33
  const error = new Error('This is a test error');
30
34
  let result;
@@ -38,8 +42,15 @@ describe('boomMiddleware', () => {
38
42
  expect(result).toBe(error);
39
43
  });
40
44
  it('should set Koa headers correctly', async () => {
41
- app.use(() => {
42
- throw unauthorized('Not authorized!', ['Basic realm="Access to test data", charset="UTF-8"']);
45
+ app.use((ctx) => {
46
+ ctx.set('www-authenticate', 'Basic realm="Access to test data", charset="UTF-8"');
47
+ ctx.response.status = 401;
48
+ ctx.response.body = {
49
+ error: 'Unauthorized',
50
+ message: 'Not authorized!',
51
+ statusCode: 401,
52
+ };
53
+ ctx.throw();
43
54
  });
44
55
  const response = await request.get('/');
45
56
  expect(response).toMatchObject({
@@ -55,4 +66,4 @@ describe('boomMiddleware', () => {
55
66
  });
56
67
  });
57
68
  });
58
- //# sourceMappingURL=boom.test.js.map
69
+ //# sourceMappingURL=error.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/node-utils",
3
- "version": "0.21.2",
3
+ "version": "0.22.0",
4
4
  "description": "NodeJS utilities used by Appsemble internally.",
5
5
  "keywords": [
6
6
  "app",
@@ -21,6 +21,12 @@
21
21
  "author": "Appsemble <info@appsemble.com> (https://appsemble.com)",
22
22
  "sideEffects": false,
23
23
  "type": "module",
24
+ "exports": {
25
+ ".": [
26
+ "./index.js",
27
+ "./index.ts"
28
+ ]
29
+ },
24
30
  "files": [
25
31
  "**/*.d.ts",
26
32
  "**/*.js",
@@ -31,11 +37,10 @@
31
37
  "test": "vitest"
32
38
  },
33
39
  "dependencies": {
34
- "@appsemble/types": "0.21.2",
35
- "@appsemble/utils": "0.21.2",
40
+ "@appsemble/types": "0.22.0",
41
+ "@appsemble/utils": "0.22.0",
36
42
  "@formatjs/fast-memoize": "^2.0.0",
37
43
  "@fortawesome/fontawesome-free": "^6.0.0",
38
- "@hapi/boom": "^10.0.0",
39
44
  "@koa/cors": "^4.0.0",
40
45
  "@odata/parser": "^0.2.0",
41
46
  "@types/koa": "^2.0.0",
@@ -86,13 +91,11 @@
86
91
  "yargs": "^17.0.0"
87
92
  },
88
93
  "devDependencies": {
89
- "@sinonjs/fake-timers": "^10.0.0",
90
94
  "@types/koa-compress": "^4.0.0",
91
95
  "@types/koa-range": "^0.3.0",
92
- "@types/sinonjs__fake-timers": "^8.0.0",
93
96
  "axios-mock-adapter": "^1.0.0",
94
97
  "axios-test-instance": "^7.0.0",
95
- "vitest": "^0.33.0"
98
+ "vitest": "^0.34.0"
96
99
  },
97
100
  "peerDependencies": {
98
101
  "prettier": "^2"
package/resource.d.ts CHANGED
@@ -10,10 +10,11 @@ import { type File } from 'koas-body-parser';
10
10
  *
11
11
  * @param app The app to get the resource definition of
12
12
  * @param resourceType The name of the resource definition to get.
13
+ * @param ctx Context used to throw back the errors.
13
14
  * @param view The view that’s being used.
14
15
  * @returns The matching resource definition.
15
16
  */
16
- export declare function getResourceDefinition(app: App, resourceType: string, view?: string): ResourceDefinition;
17
+ export declare function getResourceDefinition(app: App, resourceType: string, ctx?: Context, view?: string): ResourceDefinition;
17
18
  /**
18
19
  * Extracts the IDs of resource request body.
19
20
  *
package/resource.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { handleValidatorResult } from '@appsemble/node-utils';
3
- import { notFound } from '@hapi/boom';
4
3
  import { addMilliseconds, isPast, parseISO } from 'date-fns';
5
4
  import { ValidationError, Validator } from 'jsonschema';
6
5
  import parseDuration from 'parse-duration';
@@ -15,20 +14,54 @@ function stripResource({ $author, $created, $editor, $updated, ...data }) {
15
14
  *
16
15
  * @param app The app to get the resource definition of
17
16
  * @param resourceType The name of the resource definition to get.
17
+ * @param ctx Context used to throw back the errors.
18
18
  * @param view The view that’s being used.
19
19
  * @returns The matching resource definition.
20
20
  */
21
- export function getResourceDefinition(app, resourceType, view) {
21
+ export function getResourceDefinition(app, resourceType, ctx, view) {
22
22
  var _a;
23
23
  if (!app) {
24
- throw notFound('App not found');
24
+ if (ctx === undefined) {
25
+ throw new Error('App not found');
26
+ }
27
+ else {
28
+ ctx.response.status = 404;
29
+ ctx.response.body = {
30
+ statusCode: 404,
31
+ error: 'Not Found',
32
+ message: 'App not found',
33
+ };
34
+ ctx.throw();
35
+ }
25
36
  }
26
37
  const definition = (_a = app.definition.resources) === null || _a === void 0 ? void 0 : _a[resourceType];
27
38
  if (!definition) {
28
- throw notFound(`App does not have resources called ${resourceType}`);
39
+ if (ctx === undefined || ctx.response === undefined) {
40
+ throw new Error(`App does not have resources called ${resourceType}`);
41
+ }
42
+ else {
43
+ ctx.response.status = 404;
44
+ ctx.response.body = {
45
+ statusCode: 404,
46
+ error: 'Not Found',
47
+ message: `App does not have resources called ${resourceType}`,
48
+ };
49
+ ctx.throw();
50
+ }
29
51
  }
30
52
  if (view && !definition.views[view]) {
31
- throw notFound(`View ${view} does not exist for resource type ${resourceType}`);
53
+ if (ctx === undefined) {
54
+ throw new Error(`View ${view} does not exist for resource type ${resourceType}`);
55
+ }
56
+ else {
57
+ ctx.response.status = 404;
58
+ ctx.response.body = {
59
+ statusCode: 404,
60
+ error: 'Not Found',
61
+ message: `View ${view} does not exist for resource type ${resourceType}`,
62
+ };
63
+ ctx.throw();
64
+ }
32
65
  }
33
66
  return definition;
34
67
  }
@@ -166,7 +199,7 @@ export function processResourceBody(ctx, definition, knownAssetIds = [], knownEx
166
199
  result.errors.push(new ValidationError('is not referenced from the resource', assetId, null, ['assets', assetId], 'binary', 'format'));
167
200
  }
168
201
  }
169
- handleValidatorResult(result, 'Resource validation failed');
202
+ handleValidatorResult(ctx, result, 'Resource validation failed');
170
203
  return [resource, preparedAssets, knownAssetIds.filter((id) => !reusedAssets.has(id))];
171
204
  }
172
205
  //# sourceMappingURL=resource.js.map
@@ -1,9 +1,8 @@
1
- import { getRemapperContext, logger } from '@appsemble/node-utils';
1
+ import { getRemapperContext, logger, version } from '@appsemble/node-utils';
2
2
  import { defaultLocale, formatRequestAction, remap } from '@appsemble/utils';
3
- import { badGateway, badRequest, methodNotAllowed, notFound } from '@hapi/boom';
4
3
  import axios from 'axios';
5
4
  import { get, pick } from 'lodash-es';
6
- import pkg from '../../package.json' assert { type: 'json' };
5
+ import { EmailQuotaExceededError } from '../../EmailQuotaExceededError.js';
7
6
  /**
8
7
  * These response headers are forwarded when proxying requests.
9
8
  */
@@ -17,11 +16,31 @@ const supportedActions = ['email', 'notify', 'request'];
17
16
  async function handleEmail(ctx, app, action, options) {
18
17
  const { mailer, method, request: { body: data }, user, } = ctx;
19
18
  if (method !== 'POST') {
20
- throw methodNotAllowed('Method must be POST for email actions');
19
+ ctx.response.status = 405;
20
+ ctx.response.body = {
21
+ statusCode: 405,
22
+ message: 'Method must be POST for email actions',
23
+ error: 'Method Not Allowed',
24
+ };
25
+ ctx.throw();
21
26
  }
22
27
  const { email, reloadUser } = options;
23
28
  await reloadUser({ context: ctx });
24
- await email({ action, data, mailer, user, options, context: ctx });
29
+ try {
30
+ await email({ action, data, mailer, user, options, context: ctx });
31
+ }
32
+ catch (error) {
33
+ if (error instanceof EmailQuotaExceededError) {
34
+ ctx.response.status = 429;
35
+ ctx.response.body = {
36
+ statusCode: 429,
37
+ error: 'Too Many Requests',
38
+ message: error.message,
39
+ };
40
+ ctx.throw();
41
+ }
42
+ ctx.throw(error);
43
+ }
25
44
  ctx.status = 204;
26
45
  }
27
46
  async function handleNotify(ctx, app, action, options) {
@@ -54,7 +73,13 @@ async function handleRequestProxy(ctx, app, action, useBody, options) {
54
73
  data = JSON.parse(query.data);
55
74
  }
56
75
  catch {
57
- throw badRequest('data should be a JSON object');
76
+ ctx.response.status = 400;
77
+ ctx.response.body = {
78
+ status: 400,
79
+ message: 'data should be a JSON object.',
80
+ error: 'Bad Request',
81
+ };
82
+ ctx.throw();
58
83
  }
59
84
  }
60
85
  const { applyAppServiceSecrets, reloadUser } = options;
@@ -69,14 +94,21 @@ async function handleRequestProxy(ctx, app, action, useBody, options) {
69
94
  let axiosConfig = formatRequestAction(action, data, (remapper, d) => remap(remapper, d, remapperContext), remapperContext.context);
70
95
  axiosConfig = await applyAppServiceSecrets({ axiosConfig, context: ctx, app });
71
96
  if (axiosConfig.method.toUpperCase() !== method) {
72
- throw badRequest('Method does match the request action method');
97
+ ctx.response.status = 400;
98
+ ctx.response.body = {
99
+ statusCode: 400,
100
+ message: 'Method does not match the request action method',
101
+ error: 'Bad Request',
102
+ };
103
+ ctx.throw();
73
104
  }
74
105
  if (useBody) {
75
106
  axiosConfig.data = data;
76
107
  }
77
- axiosConfig.headers['user-agent'] = `AppsembleServer/${pkg.version}`;
108
+ axiosConfig.headers['user-agent'] = `AppsembleServer/${version}`;
78
109
  axiosConfig.responseType = 'stream';
79
110
  axiosConfig.validateStatus = () => true;
111
+ axiosConfig.decompress = false;
80
112
  let response;
81
113
  logger.verbose(`Forwarding request to ${axios.getUri(axiosConfig)}`);
82
114
  try {
@@ -84,7 +116,13 @@ async function handleRequestProxy(ctx, app, action, useBody, options) {
84
116
  }
85
117
  catch (err) {
86
118
  logger.error(err);
87
- throw badGateway();
119
+ ctx.response.status = 502;
120
+ ctx.response.body = {
121
+ statusCode: 502,
122
+ error: 'Bad Gateway',
123
+ message: 'Bad Gateway',
124
+ };
125
+ ctx.throw(502);
88
126
  }
89
127
  ctx.status = response.status;
90
128
  ctx.set(pick(response.headers, allowResponseHeaders));
@@ -95,7 +133,13 @@ function createProxyHandler(useBody, options) {
95
133
  const { pathParams: { appId, path }, } = ctx;
96
134
  const app = await options.getApp({ context: ctx, query: { where: { id: appId } } });
97
135
  if (!app) {
98
- throw notFound('App not found');
136
+ ctx.response.status = 404;
137
+ ctx.response.body = {
138
+ statusCode: 404,
139
+ error: 'Not Found',
140
+ message: 'App not found',
141
+ };
142
+ ctx.throw();
99
143
  }
100
144
  const appAction = get(app.definition, path);
101
145
  const action = supportedActions.find((act) => act === (appAction === null || appAction === void 0 ? void 0 : appAction.type));
@@ -107,7 +151,13 @@ function createProxyHandler(useBody, options) {
107
151
  case 'request':
108
152
  return handleRequestProxy(ctx, app, appAction, useBody, options);
109
153
  default:
110
- throw badRequest('path does not point to a proxyable action');
154
+ ctx.response.status = 400;
155
+ ctx.response.body = {
156
+ statusCode: 400,
157
+ message: 'path does not point to a proxyable action',
158
+ error: 'Bad Request',
159
+ };
160
+ ctx.throw();
111
161
  }
112
162
  };
113
163
  }
@@ -1,17 +1,34 @@
1
- import { notFound } from '@hapi/boom';
2
1
  export function createGetAppMember({ getApp, getAppMembers }) {
3
2
  return async (ctx) => {
4
3
  const { pathParams: { appId, memberId }, } = ctx;
5
4
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
6
5
  if (!app) {
7
- throw notFound('App not found');
6
+ ctx.response.status = 404;
7
+ ctx.response.body = {
8
+ statusCode: 404,
9
+ error: 'Not Found',
10
+ message: 'App not found',
11
+ };
12
+ ctx.throw();
8
13
  }
9
14
  if (app.definition.security === undefined) {
10
- throw notFound('App does not have a security definition');
15
+ ctx.response.status = 404;
16
+ ctx.response.body = {
17
+ statusCode: 404,
18
+ error: 'Not Found',
19
+ message: 'App does not have a security definition',
20
+ };
21
+ ctx.throw();
11
22
  }
12
23
  const appMembers = await getAppMembers({ context: ctx, app, memberId });
13
24
  if (appMembers.length !== 1) {
14
- throw notFound('App member not found');
25
+ ctx.response.status = 404;
26
+ ctx.response.body = {
27
+ statusCode: 404,
28
+ error: 'Not Found',
29
+ message: 'App member not found',
30
+ };
31
+ ctx.throw();
15
32
  }
16
33
  ctx.body = appMembers[0];
17
34
  };
@@ -1,13 +1,17 @@
1
1
  import { getAppsembleMessages, mergeMessages, } from '@appsemble/node-utils';
2
2
  import { defaultLocale, extractAppMessages, normalizeBlockName, } from '@appsemble/utils';
3
- import { badRequest, notFound } from '@hapi/boom';
4
3
  import tags from 'language-tags';
5
4
  export function createGetMessages({ getApp, getAppMessages, getBlockMessages, }) {
6
5
  return async (ctx) => {
7
6
  var _a, _b, _c, _d, _e;
8
7
  const { pathParams: { appId, language }, query: { merge, override = 'true' }, } = ctx;
9
8
  if (!tags.check(language)) {
10
- throw badRequest(`Language “${language}” is invalid`);
9
+ ctx.response.status = 400;
10
+ ctx.response.body = {
11
+ error: 'Bad Request',
12
+ message: '`Language “${language}” is invalid`',
13
+ };
14
+ ctx.throw();
11
15
  }
12
16
  const lang = language.toLowerCase();
13
17
  const baseLanguage = tags(language)
@@ -16,7 +20,13 @@ export function createGetMessages({ getApp, getAppMessages, getBlockMessages, })
16
20
  const baseLang = baseLanguage && String(baseLanguage).toLowerCase();
17
21
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
18
22
  if (!app) {
19
- throw notFound('App not found');
23
+ ctx.response.status = 404;
24
+ ctx.response.body = {
25
+ status: 404,
26
+ message: 'App not found',
27
+ error: 'Not Found',
28
+ };
29
+ ctx.throw();
20
30
  }
21
31
  const appMessages = await getAppMessages({ context: ctx, app, language });
22
32
  const blockPrefixes = [];
@@ -35,7 +45,13 @@ export function createGetMessages({ getApp, getAppMessages, getBlockMessages, })
35
45
  const blockMessages = await getBlockMessages({ context: ctx, blockQuery, baseLang, lang });
36
46
  if ((!appMessages.length || (merge && !appMessages.some((m) => m.language === lang))) &&
37
47
  lang !== (app.definition.defaultLanguage || defaultLocale)) {
38
- throw notFound(`Language “${language}” could not be found`);
48
+ ctx.response.status = 404;
49
+ ctx.response.body = {
50
+ statusCode: 404,
51
+ message: `Language “${language}” could not be found`,
52
+ error: 'Not Found',
53
+ };
54
+ ctx.throw();
39
55
  }
40
56
  const baseLanguageMessages = override === 'true' && appMessages.find((m) => m.language === baseLang);
41
57
  const languageMessages = override === 'true' && appMessages.find((m) => m.language === lang);
@@ -1,12 +1,17 @@
1
1
  import { Permission } from '@appsemble/utils';
2
- import { notFound } from '@hapi/boom';
3
2
  import { extension } from 'mime-types';
4
3
  export function createGetAssets({ checkRole, getApp, getAppAssets }) {
5
4
  return async (ctx) => {
6
5
  const { pathParams: { appId }, queryParams: { $skip, $top }, } = ctx;
7
6
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
8
7
  if (!app) {
9
- throw notFound('App not found');
8
+ ctx.response.status = 404;
9
+ ctx.response.body = {
10
+ statusCode: 404,
11
+ message: 'App not found',
12
+ error: 'Not Found',
13
+ };
14
+ ctx.throw();
10
15
  }
11
16
  const findOptions = {
12
17
  limit: $top,
@@ -22,13 +27,25 @@ export function createGetAssetById({ getApp, getAppAssets }) {
22
27
  const { pathParams: { appId, assetId }, } = ctx;
23
28
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
24
29
  if (!app) {
25
- throw notFound('App not found');
30
+ ctx.response.status = 404;
31
+ ctx.response.body = {
32
+ statusCode: 404,
33
+ message: 'App not found',
34
+ error: 'Not Found',
35
+ };
36
+ ctx.throw();
26
37
  }
27
38
  const assets = await getAppAssets({ app, context: ctx });
28
39
  // Pick asset id over asset name
29
40
  const asset = assets.find((a) => a.id === assetId) || assets.find((a) => a.name === assetId);
30
41
  if (!asset) {
31
- throw notFound('Asset not found');
42
+ ctx.response.status = 404;
43
+ ctx.response.body = {
44
+ statusCode: 404,
45
+ message: 'Asset not found',
46
+ error: 'Not Found',
47
+ };
48
+ ctx.throw();
32
49
  }
33
50
  if (assetId !== asset.id) {
34
51
  // Redirect to asset using current asset ID
@@ -60,7 +77,13 @@ export function createCreateAsset({ createAppAsset, getApp }) {
60
77
  const { pathParams: { appId }, request: { body: { file: { contents, filename, mime }, name, }, }, } = ctx;
61
78
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
62
79
  if (!app) {
63
- throw notFound('App not found');
80
+ ctx.response.status = 404;
81
+ ctx.response.body = {
82
+ statusCode: 404,
83
+ message: 'App not found',
84
+ error: 'Not Found',
85
+ };
86
+ ctx.throw();
64
87
  }
65
88
  const asset = await createAppAsset({
66
89
  app,
@@ -1,7 +1,7 @@
1
1
  export function createGetUserInfo({ getAppUserInfo }) {
2
2
  return async (ctx) => {
3
3
  const { client, user } = ctx;
4
- ctx.body = await getAppUserInfo({ context: ctx, client, user });
4
+ ctx.body = await getAppUserInfo({ context: ctx, client, user, ctx });
5
5
  };
6
6
  }
7
7
  //# sourceMappingURL=oauth2Provider.js.map
@@ -1,16 +1,28 @@
1
1
  import { getRemapperContext, getResourceDefinition, logger, processResourceBody, } from '@appsemble/node-utils';
2
2
  import { defaultLocale, remap } from '@appsemble/utils';
3
- import { badRequest, internal, notFound } from '@hapi/boom';
4
3
  function generateQuery(ctx, { parseQuery }) {
5
4
  try {
6
5
  return parseQuery({ $filter: ctx.queryParams.$filter, $orderby: ctx.queryParams.$orderby });
7
6
  }
8
7
  catch (error) {
9
8
  if (error instanceof Error) {
10
- throw badRequest('Unable to process this query', { syntaxError: error.message });
9
+ ctx.response.status = 400;
10
+ ctx.response.body = {
11
+ statusCode: 400,
12
+ error: 'Bad Request',
13
+ message: 'Unable to process this query',
14
+ data: { syntaxError: error.message },
15
+ };
16
+ ctx.throw();
11
17
  }
12
18
  logger.error(error);
13
- throw internal('Unable to process this query');
19
+ ctx.response.status = 500;
20
+ ctx.response.body = {
21
+ statusCode: 500,
22
+ error: 'Internal Server Error',
23
+ message: 'Unable to process this query',
24
+ };
25
+ ctx.throw();
14
26
  }
15
27
  }
16
28
  export function createQueryResources(options) {
@@ -26,6 +38,7 @@ export function createQueryResources(options) {
26
38
  resourceType,
27
39
  action: 'query',
28
40
  options,
41
+ ctx,
29
42
  });
30
43
  const findOptions = {
31
44
  limit: $top,
@@ -51,7 +64,7 @@ export function createQueryResources(options) {
51
64
  context: ctx,
52
65
  });
53
66
  const view = (_a = ctx.queryParams) === null || _a === void 0 ? void 0 : _a.view;
54
- const resourceDefinition = getResourceDefinition(app, resourceType, view);
67
+ const resourceDefinition = getResourceDefinition(app, resourceType, ctx, view);
55
68
  if (view) {
56
69
  const context = await getRemapperContext(app, app.definition.defaultLanguage || defaultLocale, user && {
57
70
  sub: user.id,
@@ -78,6 +91,7 @@ export function createCountResources(options) {
78
91
  action,
79
92
  resourceType,
80
93
  options,
94
+ ctx,
81
95
  });
82
96
  const { where } = generateQuery(ctx, options);
83
97
  const findOptions = {
@@ -110,13 +124,14 @@ export function createGetResourceById(options) {
110
124
  const { getApp, getAppResource, verifyResourceActionPermission } = options;
111
125
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
112
126
  const view = (_a = ctx.queryParams) === null || _a === void 0 ? void 0 : _a.view;
113
- const resourceDefinition = getResourceDefinition(app, resourceType, view);
127
+ const resourceDefinition = getResourceDefinition(app, resourceType, ctx, view);
114
128
  const userQuery = await verifyResourceActionPermission({
115
129
  app,
116
130
  context: ctx,
117
131
  action,
118
132
  resourceType,
119
133
  options,
134
+ ctx,
120
135
  });
121
136
  const findOptions = {
122
137
  where: {
@@ -135,7 +150,13 @@ export function createGetResourceById(options) {
135
150
  findOptions,
136
151
  });
137
152
  if (!resource) {
138
- throw notFound('Resource not found');
153
+ ctx.response.status = 404;
154
+ ctx.response.body = {
155
+ statusCode: 404,
156
+ error: 'Not Found',
157
+ message: 'Resource not found',
158
+ };
159
+ ctx.throw();
139
160
  }
140
161
  if (view) {
141
162
  const context = await getRemapperContext(app, app.definition.defaultLanguage || defaultLocale, user && {
@@ -157,8 +178,8 @@ export function createCreateResource(options) {
157
178
  const { createAppResourcesWithAssets, getApp, verifyResourceActionPermission } = options;
158
179
  const action = 'create';
159
180
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
160
- const resourceDefinition = getResourceDefinition(app, resourceType);
161
- await verifyResourceActionPermission({ app, context: ctx, action, resourceType, options });
181
+ const resourceDefinition = getResourceDefinition(app, resourceType, ctx);
182
+ await verifyResourceActionPermission({ app, context: ctx, action, resourceType, options, ctx });
162
183
  const [processedBody, preparedAssets] = processResourceBody(ctx, resourceDefinition);
163
184
  if (Array.isArray(processedBody) && !processedBody.length) {
164
185
  ctx.body = [];
@@ -183,13 +204,14 @@ export function createUpdateResource(options) {
183
204
  const { getApp, getAppAssets, getAppResource, updateAppResource, verifyResourceActionPermission, } = options;
184
205
  const action = 'update';
185
206
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
186
- const resourceDefinition = getResourceDefinition(app, resourceType);
207
+ const resourceDefinition = getResourceDefinition(app, resourceType, ctx);
187
208
  const userQuery = await verifyResourceActionPermission({
188
209
  app,
189
210
  context: ctx,
190
211
  action,
191
212
  resourceType,
192
213
  options,
214
+ ctx,
193
215
  });
194
216
  const findOptions = {
195
217
  where: {
@@ -208,7 +230,13 @@ export function createUpdateResource(options) {
208
230
  findOptions,
209
231
  });
210
232
  if (!oldResource) {
211
- throw notFound('Resource not found');
233
+ ctx.response.status = 404;
234
+ ctx.response.body = {
235
+ statusCode: 404,
236
+ error: 'Not Found',
237
+ message: 'Resource not found',
238
+ };
239
+ ctx.throw();
212
240
  }
213
241
  const appAssets = await getAppAssets({ context: ctx, app });
214
242
  const [processedBody, preparedAssets, deletedAssetIds] = processResourceBody(ctx, resourceDefinition, appAssets.filter((asset) => asset.resourceId === resourceId).map((asset) => asset.id), oldResource.expires);
@@ -239,6 +267,7 @@ export function createDeleteResource(options) {
239
267
  action,
240
268
  resourceType,
241
269
  options,
270
+ ctx,
242
271
  });
243
272
  const findOptions = {
244
273
  where: {
@@ -257,7 +286,13 @@ export function createDeleteResource(options) {
257
286
  findOptions,
258
287
  });
259
288
  if (!resource) {
260
- throw notFound('Resource not found');
289
+ ctx.response.status = 404;
290
+ ctx.response.body = {
291
+ statusCode: 404,
292
+ error: 'Not Found',
293
+ message: 'Resource not found',
294
+ };
295
+ ctx.throw();
261
296
  }
262
297
  await deleteAppResource({
263
298
  app,
@@ -173,7 +173,7 @@ describe('createQueryResources', () => {
173
173
  getAppUrl: mockGetAppUrl,
174
174
  getAppMessages: mockGetAppMessages,
175
175
  };
176
- const resourceDefinition = getResourceDefinition(mockApp, 'mockResourceType', 'view');
176
+ const resourceDefinition = getResourceDefinition(mockApp, 'mockResourceType', mockCtx, 'view');
177
177
  const remapperContext = await getRemapperContext(mockApp, mockApp.definition.defaultLanguage || defaultLocale, null, options, mockCtx);
178
178
  const remappedResources = mockResources.map((resource) => remap(resourceDefinition.views.view.remap, resource, remapperContext));
179
179
  const middleware = createQueryResources(options);
@@ -413,7 +413,7 @@ describe('createGetResourceById', () => {
413
413
  getAppUrl: mockGetAppUrl,
414
414
  getAppMessages: mockGetAppMessages,
415
415
  };
416
- const resourceDefinition = getResourceDefinition(mockApp, 'mockResourceType', 'view');
416
+ const resourceDefinition = getResourceDefinition(mockApp, 'mockResourceType', mockCtx, 'view');
417
417
  const remapperContext = await getRemapperContext(mockApp, mockApp.definition.defaultLanguage || defaultLocale, null, options, mockCtx);
418
418
  const remappedResource = remap(resourceDefinition.views.view.remap, mockResource, remapperContext);
419
419
  const middleware = createGetResourceById(options);
@@ -1,10 +1,15 @@
1
- import { notFound } from '@hapi/boom';
2
1
  export function createGetTeams({ getApp, getAppTeams }) {
3
2
  return async (ctx) => {
4
3
  const { pathParams: { appId }, user, } = ctx;
5
4
  const app = await getApp({ context: ctx, query: { where: { id: appId } } });
6
5
  if (!app) {
7
- throw notFound('App not found');
6
+ ctx.response.status = 404;
7
+ ctx.response.body = {
8
+ statusCode: 404,
9
+ error: 'Not Found',
10
+ message: 'App not found',
11
+ };
12
+ ctx.throw();
8
13
  }
9
14
  ctx.body = await getAppTeams({ context: ctx, app, user });
10
15
  };
@@ -1,10 +1,15 @@
1
- import { notFound } from '@hapi/boom';
2
1
  export function createBlockAssetHandler({ getBlockAsset }) {
3
2
  return async (ctx) => {
4
3
  const { params: { filename, name, version }, } = ctx;
5
4
  const blockAsset = await getBlockAsset({ filename, name, version, context: ctx });
6
5
  if (!blockAsset) {
7
- throw notFound('Block asset not found');
6
+ ctx.response.status = 404;
7
+ ctx.response.body = {
8
+ statusCode: 404,
9
+ message: 'Block asset not found',
10
+ error: 'Not Found',
11
+ };
12
+ ctx.throw();
8
13
  }
9
14
  ctx.body = blockAsset.content;
10
15
  ctx.type = blockAsset.mime;
@@ -1,10 +1,15 @@
1
- import { notFound } from '@hapi/boom';
2
1
  export function createBlockCssHandler({ getApp, getAppBlockStyles }) {
3
2
  return async (ctx) => {
4
3
  const { params: { name }, } = ctx;
5
4
  const app = await getApp({ context: ctx });
6
5
  if (!app) {
7
- throw notFound('App not found');
6
+ ctx.response.status = 404;
7
+ ctx.response.body = {
8
+ statusCode: 404,
9
+ error: 'Not Found',
10
+ message: 'App not found',
11
+ };
12
+ ctx.throw();
8
13
  }
9
14
  const appBlockStyles = await getAppBlockStyles({ app, name, context: ctx });
10
15
  const [style] = appBlockStyles;
@@ -1,7 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
- import { logger } from '@appsemble/node-utils';
2
+ import { bulmaVersion, logger } from '@appsemble/node-utils';
3
3
  import { baseTheme } from '@appsemble/utils';
4
- import bulma from 'bulma/package.json' assert { type: 'json' };
5
4
  import { renderSync } from 'sass';
6
5
  import stripBom from 'strip-bom';
7
6
  const require = createRequire(import.meta.url);
@@ -76,7 +75,7 @@ export function createBulmaHandler({ createTheme, getTheme }) {
76
75
  return async (ctx) => {
77
76
  const { query } = ctx;
78
77
  const theme = {
79
- bulmaVersion: bulma.version,
78
+ bulmaVersion,
80
79
  primaryColor: (getQueryParameter(query.primaryColor) || baseTheme.primaryColor).toLowerCase(),
81
80
  linkColor: (getQueryParameter(query.linkColor) || baseTheme.linkColor).toLowerCase(),
82
81
  successColor: (getQueryParameter(query.successColor) || baseTheme.successColor).toLowerCase(),
@@ -1,9 +1,14 @@
1
- import { notFound } from '@hapi/boom';
2
1
  export function createCssHandler(type, { getAppStyles }) {
3
2
  return async (ctx) => {
4
3
  const app = await getAppStyles({ context: ctx, query: { attributes: [type], raw: true } });
5
4
  if (!app) {
6
- throw notFound('App not found');
5
+ ctx.response.status = 404;
6
+ ctx.response.body = {
7
+ statusCode: 404,
8
+ error: 'Not Found',
9
+ message: 'App not found',
10
+ };
11
+ ctx.throw();
7
12
  }
8
13
  ctx.body = app[type];
9
14
  ctx.type = 'css';
@@ -1,8 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { dirname } from 'node:path';
3
- import { tinyRouter } from '@appsemble/node-utils';
3
+ import { faVersion, tinyRouter } from '@appsemble/node-utils';
4
4
  import { noop, partialNormalized, partialSemver } from '@appsemble/utils';
5
- import faPkg from '@fortawesome/fontawesome-free/package.json' assert { type: 'json' };
6
5
  import mount from 'koa-mount';
7
6
  import serve from 'koa-static';
8
7
  import { createBlockAssetHandler } from './blockAssetHandler.js';
@@ -50,7 +49,7 @@ export function createAppRouter(options) {
50
49
  },
51
50
  {
52
51
  route: /^\/fa\//,
53
- get: mount(`/fa/${faPkg.version}`, serve(dirname(require.resolve('@fortawesome/fontawesome-free/package.json')))),
52
+ get: mount(`/fa/${faVersion}`, serve(dirname(require.resolve('@fortawesome/fontawesome-free/package.json')))),
54
53
  },
55
54
  {
56
55
  route: '/core.css',
@@ -1,11 +1,10 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { createThemeURL, defaultLocale, getAppBlocks, mergeThemes } from '@appsemble/utils';
3
- import faPkg from '@fortawesome/fontawesome-free/package.json' assert { type: 'json' };
4
- import bulmaPkg from 'bulma/package.json' assert { type: 'json' };
5
3
  import { organizationBlocklist } from '../../../organizationBlocklist.js';
6
4
  import { makeCSP, render } from '../../../render.js';
7
- export const bulmaURL = `/bulma/${bulmaPkg.version}/bulma.min.css`;
8
- export const faURL = `/fa/${faPkg.version}/css/all.min.css`;
5
+ import { bulmaVersion, faVersion } from '../../../versions.cjs';
6
+ export const bulmaURL = `/bulma/${bulmaVersion}/bulma.min.css`;
7
+ export const faURL = `/fa/${faVersion}/css/all.min.css`;
9
8
  export function createIndexHandler({ createSettings, getApp, getAppDetails, getAppMessages, getAppUrl, getCsp, getHost, }) {
10
9
  return async (ctx) => {
11
10
  var _a;
@@ -1,12 +1,17 @@
1
1
  import { baseTheme, normalize } from '@appsemble/utils';
2
- import { notFound } from '@hapi/boom';
3
2
  import { extension } from 'mime-types';
4
3
  const iconSizes = [48, 144, 192, 512];
5
4
  export function createManifestHandler({ getApp, getAppScreenshots }) {
6
5
  return async (ctx) => {
7
6
  const app = await getApp({ context: ctx });
8
7
  if (!app) {
9
- throw notFound('App not found');
8
+ ctx.response.status = 404;
9
+ ctx.response.body = {
10
+ statusCode: 404,
11
+ message: 'Block asset not found',
12
+ error: 'Not Found',
13
+ };
14
+ ctx.throw();
10
15
  }
11
16
  const { defaultPage, description, name, theme = baseTheme } = app.definition;
12
17
  const { themeColor = '#ffffff', splashColor = themeColor } = theme;
@@ -1,15 +1,26 @@
1
- import { notFound } from '@hapi/boom';
2
1
  export function createScreenshotHandler({ getApp, getAppScreenshots }) {
3
2
  return async (ctx) => {
4
3
  const { id } = ctx.params;
5
4
  const app = await getApp({ context: ctx });
6
5
  if (!app) {
7
- throw notFound('App not found');
6
+ ctx.response.status = 404;
7
+ ctx.response.body = {
8
+ statusCode: 404,
9
+ message: 'App not found',
10
+ error: 'Not Found',
11
+ };
12
+ ctx.throw();
8
13
  }
9
14
  const appScreenshots = await getAppScreenshots({ app, context: ctx });
10
15
  const appScreenshot = appScreenshots.find((screenshot) => screenshot.id === Number.parseInt(id));
11
16
  if (!appScreenshot) {
12
- throw notFound('Screenshot not found');
17
+ ctx.response.status = 404;
18
+ ctx.response.body = {
19
+ statusCode: 404,
20
+ message: 'Screenshot not found',
21
+ error: 'Not Found',
22
+ };
23
+ ctx.throw();
13
24
  }
14
25
  const { mime, screenshot } = appScreenshot;
15
26
  ctx.type = mime;
@@ -8,7 +8,15 @@ export function createServiceWorkerHandler({ getApp, getBlocksAssetsPaths }) {
8
8
  ? readFile(new URL('../../../../../dist/app/service-worker.js', import.meta.url), 'utf8')
9
9
  : ctx.fs.promises.readFile(filename, 'utf8'));
10
10
  const app = await getApp({ context: ctx });
11
- ctx.assert(app, 404, 'App does not exist.');
11
+ if (!app) {
12
+ ctx.response.status = 404;
13
+ ctx.response.body = {
14
+ statusCode: 404,
15
+ message: 'App does not exist.',
16
+ error: 'Not Found',
17
+ };
18
+ ctx.throw();
19
+ }
12
20
  const identifiableBlocks = getAppBlocks(app.definition);
13
21
  const blocksAssetsPaths = await getBlocksAssetsPaths({ identifiableBlocks, context: ctx });
14
22
  ctx.body = `const blockAssets=${JSON.stringify(blocksAssetsPaths)};${serviceWorker}`;
package/server/types.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { type App, type AppMember, type AppMessages, type Asset, type BlockConfig, type BlockDefinition, type BlockManifest, type EmailActionDefinition, type Resource, type ResourceDefinition, type TeamMember, type Theme as ThemeType, type UserInfo } from '@appsemble/types';
4
4
  import { type IdentifiableBlock, type Permission } from '@appsemble/utils';
5
5
  import { type RawAxiosRequestConfig } from 'axios';
6
- import { type DefaultContext as DefaultContextInterface, type DefaultState, type ParameterizedContext } from 'koa';
6
+ import { type Context, type DefaultContext as DefaultContextInterface, type DefaultState, type ParameterizedContext } from 'koa';
7
7
  export interface UtilsUser {
8
8
  id: string;
9
9
  name: string;
@@ -19,6 +19,7 @@ declare module 'koa' {
19
19
  body: any;
20
20
  }
21
21
  interface DefaultContext {
22
+ appPath: string;
22
23
  appHost: string;
23
24
  appsembleApp: App;
24
25
  appBlocks: BlockManifest[];
@@ -130,6 +131,7 @@ export interface GetAppUserInfoParams {
130
131
  app: App;
131
132
  } | {};
132
133
  user: UtilsUser;
134
+ ctx: Context;
133
135
  }
134
136
  export interface ExtendedTeam extends TeamMember {
135
137
  size: number;
@@ -198,6 +200,7 @@ export interface VerifyResourceActionPermissionParams {
198
200
  resourceType: string;
199
201
  action: Action;
200
202
  options: Options;
203
+ ctx: Context;
201
204
  }
202
205
  export interface ApplyAppServiceSecretsParams {
203
206
  context: ParameterizedContext<DefaultState, DefaultContextInterface, any>;
package/tinyRouter.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { has } from '@appsemble/utils';
2
- import { methodNotAllowed } from '@hapi/boom';
3
2
  /**
4
3
  * A tiny dynamic router middleware for GET requests.
5
4
  *
@@ -23,7 +22,13 @@ export function tinyRouter(routes) {
23
22
  let m = method.toLowerCase();
24
23
  if (!has(result, m)) {
25
24
  if (!has(result, 'any')) {
26
- throw methodNotAllowed();
25
+ ctx.response.status = 405;
26
+ ctx.response.body = {
27
+ statusCode: 405,
28
+ error: 'Method Not Allowed',
29
+ message: 'Method not allowed',
30
+ };
31
+ ctx.throw();
27
32
  }
28
33
  m = 'any';
29
34
  }
@@ -1,6 +1,7 @@
1
1
  import { noop } from '@appsemble/utils';
2
2
  import { request, setTestApp } from 'axios-test-instance';
3
3
  import Koa from 'koa';
4
+ import { errorMiddleware } from './index.js';
4
5
  import { tinyRouter } from './tinyRouter.js';
5
6
  let app;
6
7
  let context;
@@ -24,10 +25,7 @@ it('should assign the match group to params', async () => {
24
25
  expect(context.params).toStrictEqual({ foo: '1', bar: '2' });
25
26
  });
26
27
  it('should throw method not allowed if a URL is matched, but not for the given method', async () => {
27
- let error;
28
- app.on('error', (err) => {
29
- error = err;
30
- });
28
+ app.use(errorMiddleware());
31
29
  app.use(tinyRouter([
32
30
  {
33
31
  route: '/',
@@ -35,8 +33,7 @@ it('should throw method not allowed if a URL is matched, but not for the given m
35
33
  },
36
34
  ]));
37
35
  await request.post('/');
38
- expect(error.isBoom).toBe(true);
39
- expect(error.output.statusCode).toBe(405);
36
+ expect(context.status).toBe(405);
40
37
  });
41
38
  it('should fall back to the any handler if it exists', async () => {
42
39
  const any = vi.fn();
@@ -1,7 +0,0 @@
1
- import { type Middleware } from 'koa';
2
- /**
3
- * Koa middleware for handling Boom errors.
4
- *
5
- * @returns Koa middleware that converts Boom errors into an HTTP response.
6
- */
7
- export declare function boomMiddleware(): Middleware;
@@ -1,32 +0,0 @@
1
- import { SCIMError } from '../scim.js';
2
- /**
3
- * Koa middleware for handling Boom errors.
4
- *
5
- * @returns Koa middleware that converts Boom errors into an HTTP response.
6
- */
7
- export function boomMiddleware() {
8
- return async (ctx, next) => {
9
- try {
10
- await next();
11
- }
12
- catch (error) {
13
- if (error instanceof SCIMError) {
14
- ctx.status = Number(error.status);
15
- ctx.body = error;
16
- return;
17
- }
18
- const err = error;
19
- if (!err.isBoom) {
20
- throw err;
21
- }
22
- const { output } = err;
23
- ctx.status = output.statusCode;
24
- ctx.body = output.payload;
25
- if (err.data) {
26
- ctx.body.data = err.data;
27
- }
28
- ctx.set(output.headers);
29
- }
30
- };
31
- }
32
- //# sourceMappingURL=boom.js.map
@@ -1,2 +0,0 @@
1
- declare const _default: import("vitest/dist/config.js").UserProjectConfigExport;
2
- export default _default;
File without changes