@nrfcloud/lambda-helpers 1.0.28
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/LICENSE +29 -0
- package/README.md +34 -0
- package/npm/aProblem.d.ts +4 -0
- package/npm/aProblem.js +12 -0
- package/npm/aResponse.d.ts +5 -0
- package/npm/aResponse.js +15 -0
- package/npm/addCORSHeaders.d.ts +3 -0
- package/npm/addCORSHeaders.js +21 -0
- package/npm/addVersionHeader.d.ts +3 -0
- package/npm/addVersionHeader.js +16 -0
- package/npm/corsHeaders.d.ts +11 -0
- package/npm/corsHeaders.js +34 -0
- package/npm/corsOPTIONS.d.ts +3 -0
- package/npm/corsOPTIONS.js +30 -0
- package/npm/corsResponse.d.ts +4 -0
- package/npm/corsResponse.js +5 -0
- package/npm/export.d.ts +15 -0
- package/npm/export.js +15 -0
- package/npm/logger.d.ts +7 -0
- package/npm/logger.js +15 -0
- package/npm/metrics.d.ts +9 -0
- package/npm/metrics.js +24 -0
- package/npm/parseHeaders.d.ts +8 -0
- package/npm/parseHeaders.js +20 -0
- package/npm/problemResponse.d.ts +3 -0
- package/npm/problemResponse.js +30 -0
- package/npm/requestLogger.d.ts +2 -0
- package/npm/requestLogger.js +4 -0
- package/npm/tryAsJSON.d.ts +1 -0
- package/npm/tryAsJSON.js +8 -0
- package/npm/validateInput.d.ts +18 -0
- package/npm/validateInput.js +44 -0
- package/npm/validateResponse.d.ts +11 -0
- package/npm/validateResponse.js +35 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Nordic Semiconductor ASA | nordicsemi.no
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# `@nrfcloud/lambda-helpers`
|
|
2
|
+
|
|
3
|
+
<https://www.npmjs.com/package/@nrfcloud/lambda-helpers>
|
|
4
|
+
|
|
5
|
+
Helper functions for AWS Lambda functions used in nRF Cloud.
|
|
6
|
+
|
|
7
|
+
## Install with NPM
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i (--save-prod|--save-dev) @nrfcloud/lambda-helpers
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { MetricUnit } from "@aws-lambda-powertools/metrics";
|
|
17
|
+
import { logMetrics } from "@aws-lambda-powertools/metrics/middleware";
|
|
18
|
+
import middy from "@middy/core";
|
|
19
|
+
import { metricsForComponent } from "@nrfcloud/lambda-helpers";
|
|
20
|
+
|
|
21
|
+
const { track, metrics } = metricsForComponent("my-function", "my-service");
|
|
22
|
+
|
|
23
|
+
export const handler = middy()
|
|
24
|
+
.use(logMetrics(metrics))
|
|
25
|
+
.handler(async () => {
|
|
26
|
+
track("some:metric", MetricUnit.Count, 1);
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## TypeScript 6 and 7
|
|
31
|
+
|
|
32
|
+
This repo
|
|
33
|
+
[runs TypeScript 6 and 7 side by side](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0),
|
|
34
|
+
[so that eslint works](https://github.com/typescript-eslint/typescript-eslint/issues/10940#issuecomment-4922812181).
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ProblemDetail } from '@nrfcloud/problem-detail';
|
|
2
|
+
import type { Static } from '@sinclair/typebox';
|
|
3
|
+
import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
4
|
+
export declare const aProblem: (problem: Omit<Static<typeof ProblemDetail>, '@context'>, cacheForSeconds?: number) => APIGatewayProxyStructuredResultV2;
|
package/npm/aProblem.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context as ProblemDetailContext } from '@nrfcloud/problem-detail';
|
|
2
|
+
export const aProblem = (problem, cacheForSeconds = 0)=>({
|
|
3
|
+
statusCode: problem.status,
|
|
4
|
+
headers: {
|
|
5
|
+
'content-type': 'application/problem+json',
|
|
6
|
+
'Cache-Control': cacheForSeconds > 0 ? `public, max-age=${cacheForSeconds}` : 'no-store'
|
|
7
|
+
},
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
'@context': ProblemDetailContext,
|
|
10
|
+
...problem
|
|
11
|
+
})
|
|
12
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HttpStatusCode } from '@nrfcloud/problem-detail';
|
|
2
|
+
import type { APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
3
|
+
export declare const aResponse: (status: HttpStatusCode, result?: {
|
|
4
|
+
'@context': URL;
|
|
5
|
+
} & Record<string, unknown>, cacheForSeconds?: number, headers?: APIGatewayProxyStructuredResultV2['headers']) => APIGatewayProxyStructuredResultV2;
|
package/npm/aResponse.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const aResponse = (status, result, cacheForSeconds = 60, headers)=>{
|
|
2
|
+
const body = result !== undefined ? JSON.stringify(result) : undefined;
|
|
3
|
+
return {
|
|
4
|
+
statusCode: status,
|
|
5
|
+
headers: {
|
|
6
|
+
...body !== undefined ? {
|
|
7
|
+
'content-type': 'application/json'
|
|
8
|
+
} : {},
|
|
9
|
+
'content-length': `${body?.length ?? 0}`,
|
|
10
|
+
'Cache-Control': cacheForSeconds > 0 ? `public, max-age=${cacheForSeconds}` : 'no-store',
|
|
11
|
+
...headers ?? {}
|
|
12
|
+
},
|
|
13
|
+
body
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MiddlewareObj } from '@middy/core';
|
|
2
|
+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
3
|
+
export declare const addCORSHeaders: (AccessControlAllowOrigin?: string, AccessControlExposeHeaders?: Array<string>) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const addCORSHeaders = (AccessControlAllowOrigin = '*', AccessControlExposeHeaders = [
|
|
2
|
+
'x-amzn-requestid',
|
|
3
|
+
'etag',
|
|
4
|
+
'apigw-requestid'
|
|
5
|
+
])=>{
|
|
6
|
+
const setCORSHeaders = async (req)=>{
|
|
7
|
+
if (req.response === null) return;
|
|
8
|
+
req.response = {
|
|
9
|
+
...req.response,
|
|
10
|
+
headers: {
|
|
11
|
+
...req.response.headers ?? {},
|
|
12
|
+
'Access-Control-Allow-Origin': AccessControlAllowOrigin,
|
|
13
|
+
'Access-Control-Expose-Headers': AccessControlExposeHeaders.join(', ')
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
after: setCORSHeaders,
|
|
19
|
+
onError: setCORSHeaders
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MiddlewareObj } from '@middy/core';
|
|
2
|
+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
3
|
+
export declare const addVersionHeader: (version: string) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const addVersionHeader = (version)=>{
|
|
2
|
+
const setVersionHeader = async (req)=>{
|
|
3
|
+
if (req.response === null) return;
|
|
4
|
+
req.response = {
|
|
5
|
+
...req.response,
|
|
6
|
+
headers: {
|
|
7
|
+
...req.response.headers ?? {},
|
|
8
|
+
'x-backend-version': version
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
after: setVersionHeader,
|
|
14
|
+
onError: setVersionHeader
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { APIGatewayProxyEventHeaders } from 'aws-lambda';
|
|
2
|
+
export declare const corsHeaders: ({ headers, }: {
|
|
3
|
+
headers: APIGatewayProxyEventHeaders;
|
|
4
|
+
}, allowedMethods?: string[], cacheForSeconds?: number) => {
|
|
5
|
+
'Access-Control-Allow-Headers': string;
|
|
6
|
+
'Access-Control-Expose-Headers': string;
|
|
7
|
+
'Access-Control-Allow-Methods': string;
|
|
8
|
+
'Access-Control-Allow-Origin': string;
|
|
9
|
+
'Access-Control-Max-Age': number;
|
|
10
|
+
Vary: 'Origin';
|
|
11
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parseHeaders } from './parseHeaders.js';
|
|
2
|
+
const allowedDomains = [
|
|
3
|
+
/^https?:\/\/localhost:/,
|
|
4
|
+
/^https:\/\/hello\.nrfcloud\.com$/
|
|
5
|
+
];
|
|
6
|
+
const defaultOrigin = 'https://hello.nrfcloud.com';
|
|
7
|
+
const origin = (event)=>{
|
|
8
|
+
const origin = parseHeaders(event.headers).get('origin') ?? defaultOrigin.toString();
|
|
9
|
+
if (allowedDomains.find((rx)=>rx.test(origin)) !== undefined) return origin;
|
|
10
|
+
return defaultOrigin;
|
|
11
|
+
};
|
|
12
|
+
export const corsHeaders = ({ headers }, allowedMethods = [
|
|
13
|
+
'PUT',
|
|
14
|
+
'DELETE',
|
|
15
|
+
'POST',
|
|
16
|
+
'GET',
|
|
17
|
+
'PATCH'
|
|
18
|
+
], cacheForSeconds = 600)=>({
|
|
19
|
+
'Access-Control-Allow-Origin': origin({
|
|
20
|
+
headers
|
|
21
|
+
}),
|
|
22
|
+
'Access-Control-Allow-Methods': allowedMethods.join(', '),
|
|
23
|
+
'Access-Control-Allow-Headers': Array.from(new Set([
|
|
24
|
+
'content-type',
|
|
25
|
+
'accept',
|
|
26
|
+
'if-match',
|
|
27
|
+
'authorization',
|
|
28
|
+
'origin'
|
|
29
|
+
])).map((h)=>h.trim()).sort((h1, h2)=>h1.localeCompare(h2)).join(', '),
|
|
30
|
+
'Access-Control-Expose-Headers': 'x-amzn-requestid, etag, apigw-requestid',
|
|
31
|
+
'Access-Control-Max-Age': cacheForSeconds,
|
|
32
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#cors_and_caching
|
|
33
|
+
Vary: 'Origin'
|
|
34
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MiddlewareObj } from '@middy/core';
|
|
2
|
+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
3
|
+
export declare const corsOPTIONS: (...allowedMethods: string[]) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { corsHeaders } from './corsHeaders.js';
|
|
2
|
+
export const corsOPTIONS = (...allowedMethods)=>{
|
|
3
|
+
const setCorsHeaders = async (req)=>{
|
|
4
|
+
if (req.response === null) {
|
|
5
|
+
console.error(`[corsOPTIONS]`, `Response is null`);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
req.response = {
|
|
9
|
+
...req.response,
|
|
10
|
+
headers: {
|
|
11
|
+
...req.response?.headers ?? {},
|
|
12
|
+
...corsHeaders(req.event, allowedMethods)
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
before: async (req)=>{
|
|
18
|
+
if (req.event.requestContext.http.method === 'OPTIONS') {
|
|
19
|
+
console.debug(`[corsOPTIONS]`, `Handling OPTIONS request`);
|
|
20
|
+
return {
|
|
21
|
+
statusCode: 200,
|
|
22
|
+
headers: corsHeaders(req.event, allowedMethods)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
},
|
|
27
|
+
after: setCorsHeaders,
|
|
28
|
+
onError: setCorsHeaders
|
|
29
|
+
};
|
|
30
|
+
};
|
package/npm/export.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './addCORSHeaders.ts';
|
|
2
|
+
export * from './addVersionHeader.ts';
|
|
3
|
+
export * from './aProblem.ts';
|
|
4
|
+
export * from './aResponse.ts';
|
|
5
|
+
export * from './corsHeaders.ts';
|
|
6
|
+
export * from './corsOPTIONS.ts';
|
|
7
|
+
export * from './corsResponse.ts';
|
|
8
|
+
export * from './logger.ts';
|
|
9
|
+
export * from './metrics.ts';
|
|
10
|
+
export * from './parseHeaders.ts';
|
|
11
|
+
export * from './problemResponse.ts';
|
|
12
|
+
export * from './requestLogger.ts';
|
|
13
|
+
export * from './tryAsJSON.ts';
|
|
14
|
+
export * from './validateInput.ts';
|
|
15
|
+
export * from './validateResponse.ts';
|
package/npm/export.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './addCORSHeaders.js';
|
|
2
|
+
export * from './addVersionHeader.js';
|
|
3
|
+
export * from './aProblem.js';
|
|
4
|
+
export * from './aResponse.js';
|
|
5
|
+
export * from './corsHeaders.js';
|
|
6
|
+
export * from './corsOPTIONS.js';
|
|
7
|
+
export * from './corsResponse.js';
|
|
8
|
+
export * from './logger.js';
|
|
9
|
+
export * from './metrics.js';
|
|
10
|
+
export * from './parseHeaders.js';
|
|
11
|
+
export * from './problemResponse.js';
|
|
12
|
+
export * from './requestLogger.js';
|
|
13
|
+
export * from './tryAsJSON.js';
|
|
14
|
+
export * from './validateInput.js';
|
|
15
|
+
export * from './validateResponse.js';
|
package/npm/logger.d.ts
ADDED
package/npm/logger.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const logger = (serviceName)=>({
|
|
2
|
+
error: (message, detail)=>{
|
|
3
|
+
console.error(`[${serviceName}]`, message);
|
|
4
|
+
if (detail !== undefined) console.error(JSON.stringify(detail));
|
|
5
|
+
},
|
|
6
|
+
debug: (...args)=>{
|
|
7
|
+
console.debug(`[${serviceName}]`, ...args.map((a)=>JSON.stringify(a)));
|
|
8
|
+
},
|
|
9
|
+
info: (...args)=>{
|
|
10
|
+
console.log(`[${serviceName}]`, ...args.map((a)=>JSON.stringify(a)));
|
|
11
|
+
},
|
|
12
|
+
warn: (...args)=>{
|
|
13
|
+
console.warn(`[${serviceName}]`, ...args.map((a)=>JSON.stringify(a)));
|
|
14
|
+
}
|
|
15
|
+
});
|
package/npm/metrics.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Metrics } from '@aws-lambda-powertools/metrics';
|
|
2
|
+
export type AddMetricsFn = (...args: Parameters<Metrics['addMetric']>) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Manages the instantiation of a Metrics object (unless Metrics are disabled)
|
|
5
|
+
*/
|
|
6
|
+
export declare const metricsForComponent: (component: string, namespace?: string) => {
|
|
7
|
+
metrics: Metrics;
|
|
8
|
+
track: AddMetricsFn;
|
|
9
|
+
};
|
package/npm/metrics.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Metrics } from '@aws-lambda-powertools/metrics';
|
|
2
|
+
const registry = {};
|
|
3
|
+
/**
|
|
4
|
+
* Manages the instantiation of a Metrics object (unless Metrics are disabled)
|
|
5
|
+
*/ export const metricsForComponent = (component, namespace = 'nrfcloud')=>{
|
|
6
|
+
const metricsEnabled = process.env.DISABLE_METRICS !== '1';
|
|
7
|
+
if (registry[component] === undefined) {
|
|
8
|
+
console.debug(`[Metrics]`, metricsEnabled ? `Enabled` : `Disabled`);
|
|
9
|
+
registry[component] = new Metrics({
|
|
10
|
+
namespace,
|
|
11
|
+
serviceName: component
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const metrics = registry[component];
|
|
15
|
+
return {
|
|
16
|
+
metrics,
|
|
17
|
+
track: (...args)=>{
|
|
18
|
+
if (!metricsEnabled) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
metrics.addMetric(...args);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a Map of headers with keys in lowercase.
|
|
3
|
+
* This is useful for consistent header handling, especially in environments
|
|
4
|
+
* where header keys may be case-insensitive.
|
|
5
|
+
*/
|
|
6
|
+
export declare const parseHeaders: (headers: Record<string, string | undefined> | null) => ParsedHeaders;
|
|
7
|
+
export type ParsedHeaders = Pick<Map<string, string | null>, 'get' | 'has' | 'size' | 'keys' | 'values' | 'entries' | 'forEach'>;
|
|
8
|
+
export declare const CaseInsensitiveMap: (map: Map<string, string | null>) => ParsedHeaders;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a Map of headers with keys in lowercase.
|
|
3
|
+
* This is useful for consistent header handling, especially in environments
|
|
4
|
+
* where header keys may be case-insensitive.
|
|
5
|
+
*/ export const parseHeaders = (headers)=>{
|
|
6
|
+
if (headers === null) return new Map();
|
|
7
|
+
return CaseInsensitiveMap(Object.entries(headers).reduce((h, [key, value])=>{
|
|
8
|
+
h.set(key.toLowerCase(), value ?? null);
|
|
9
|
+
return h;
|
|
10
|
+
}, new Map()));
|
|
11
|
+
};
|
|
12
|
+
export const CaseInsensitiveMap = (map)=>({
|
|
13
|
+
get: (key)=>map.get(key.toLowerCase()),
|
|
14
|
+
has: (key)=>map.has(key.toLowerCase()),
|
|
15
|
+
size: map.size,
|
|
16
|
+
keys: ()=>map.keys(),
|
|
17
|
+
values: ()=>map.values(),
|
|
18
|
+
entries: ()=>map.entries(),
|
|
19
|
+
forEach: (callback, thisArg)=>map.forEach(callback, thisArg)
|
|
20
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MiddlewareObj } from '@middy/core';
|
|
2
|
+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Context as LambdaContext } from 'aws-lambda';
|
|
3
|
+
export declare const problemResponse: () => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Error, LambdaContext>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HttpStatusCode, ProblemDetailError } from '@nrfcloud/problem-detail';
|
|
2
|
+
import { formatTypeBoxErrors } from '@nrfcloud/validate-with-typebox';
|
|
3
|
+
import { aProblem } from './aProblem.js';
|
|
4
|
+
import { ValidationFailedError } from './validateInput.js';
|
|
5
|
+
import { ResponseValidationFailedError } from './validateResponse.js';
|
|
6
|
+
export const problemResponse = ()=>({
|
|
7
|
+
onError: async (req)=>{
|
|
8
|
+
if (req.response !== undefined) return;
|
|
9
|
+
if (req.error instanceof ResponseValidationFailedError) {
|
|
10
|
+
req.response = aProblem({
|
|
11
|
+
title: 'Response validation failed',
|
|
12
|
+
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
13
|
+
detail: formatTypeBoxErrors(req.error.errors)
|
|
14
|
+
});
|
|
15
|
+
} else if (req.error instanceof ValidationFailedError) {
|
|
16
|
+
req.response = aProblem({
|
|
17
|
+
title: 'Validation failed',
|
|
18
|
+
status: HttpStatusCode.BAD_REQUEST,
|
|
19
|
+
detail: formatTypeBoxErrors(req.error.errors)
|
|
20
|
+
});
|
|
21
|
+
} else if (req.error instanceof ProblemDetailError) {
|
|
22
|
+
req.response = aProblem(req.error.problem);
|
|
23
|
+
} else {
|
|
24
|
+
req.response = aProblem({
|
|
25
|
+
title: req.error instanceof Error ? req.error.message : 'Internal Server Error',
|
|
26
|
+
status: HttpStatusCode.INTERNAL_SERVER_ERROR
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const tryAsJSON: (body: unknown) => Record<string, any> | null;
|
package/npm/tryAsJSON.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MiddlewareObj } from '@middy/core';
|
|
2
|
+
import type { Static, StaticDecode, TSchema } from '@sinclair/typebox';
|
|
3
|
+
import type { ValueError } from '@sinclair/typebox/compiler';
|
|
4
|
+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Context } from 'aws-lambda';
|
|
5
|
+
export declare class ValidationFailedError extends Error {
|
|
6
|
+
readonly errors: ValueError[];
|
|
7
|
+
constructor(errors: ValueError[], message?: string);
|
|
8
|
+
}
|
|
9
|
+
export declare const validateInput: <Schema extends TSchema>(schema: Schema, mapInput?: (e: APIGatewayProxyEventV2) => unknown) => MiddlewareObj<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Error, Context & ValidInput<Schema>>;
|
|
10
|
+
export type ValidInput<Schema extends TSchema> = {
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Use `decodedInput` instead
|
|
13
|
+
*
|
|
14
|
+
* This will be removed in the next major version.
|
|
15
|
+
*/
|
|
16
|
+
validInput: Static<Schema>;
|
|
17
|
+
decodedInput: StaticDecode<Schema>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { formatTypeBoxErrors, validateWithTypeBox } from '@nrfcloud/validate-with-typebox';
|
|
2
|
+
import { Value } from '@sinclair/typebox/value';
|
|
3
|
+
import { parseHeaders } from './parseHeaders.js';
|
|
4
|
+
import { tryAsJSON } from './tryAsJSON.js';
|
|
5
|
+
export class ValidationFailedError extends Error {
|
|
6
|
+
errors;
|
|
7
|
+
constructor(errors, message = 'Validation failed'){
|
|
8
|
+
super(message);
|
|
9
|
+
this.errors = errors;
|
|
10
|
+
this.name = 'ValidationFailedError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const validateInput = (schema, mapInput)=>{
|
|
14
|
+
const v = validateWithTypeBox(schema);
|
|
15
|
+
return {
|
|
16
|
+
before: async (req)=>{
|
|
17
|
+
let reqBody = {};
|
|
18
|
+
const headers = parseHeaders(req.event.headers);
|
|
19
|
+
const contentType = headers.get('content-type') ?? '';
|
|
20
|
+
if (contentType.includes('application/json') && (req.event.body?.length ?? 0) >= 2 // should be at least '{}'
|
|
21
|
+
) {
|
|
22
|
+
reqBody = tryAsJSON(req.event.body) ?? {};
|
|
23
|
+
}
|
|
24
|
+
const input = mapInput?.(req.event) ?? {
|
|
25
|
+
...req.event.pathParameters ?? {},
|
|
26
|
+
...req.event.queryStringParameters ?? {},
|
|
27
|
+
...reqBody
|
|
28
|
+
};
|
|
29
|
+
console.debug(`[validateInput]`, 'input', JSON.stringify(input));
|
|
30
|
+
const maybeValidInput = v(input);
|
|
31
|
+
if ('errors' in maybeValidInput) {
|
|
32
|
+
console.debug(`[validateInput]`, `Input not valid`, JSON.stringify({
|
|
33
|
+
input,
|
|
34
|
+
errors: formatTypeBoxErrors(maybeValidInput.errors)
|
|
35
|
+
}), schema.title);
|
|
36
|
+
throw new ValidationFailedError(maybeValidInput.errors);
|
|
37
|
+
}
|
|
38
|
+
console.debug(`[validateInput]`, `Input is valid`, schema.title);
|
|
39
|
+
req.context.validInput = maybeValidInput.value;
|
|
40
|
+
req.context.decodedInput = Value.Decode(schema, maybeValidInput.value);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type middy from '@middy/core';
|
|
2
|
+
import type { TSchema } from '@sinclair/typebox';
|
|
3
|
+
import type { ValueError } from '@sinclair/typebox/errors';
|
|
4
|
+
import { ValidationFailedError } from './validateInput.ts';
|
|
5
|
+
export declare class ResponseValidationFailedError extends ValidationFailedError {
|
|
6
|
+
constructor(errors: ValueError[]);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validate responses created with `aResponse`
|
|
10
|
+
*/
|
|
11
|
+
export declare const validateResponse: <ResponseSchema extends TSchema>(schema: ResponseSchema) => middy.MiddlewareObj;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { formatTypeBoxErrors, validateWithTypeBox } from '@nrfcloud/validate-with-typebox';
|
|
2
|
+
import { parseHeaders } from './parseHeaders.js';
|
|
3
|
+
import { tryAsJSON } from './tryAsJSON.js';
|
|
4
|
+
import { ValidationFailedError } from './validateInput.js';
|
|
5
|
+
export class ResponseValidationFailedError extends ValidationFailedError {
|
|
6
|
+
constructor(errors){
|
|
7
|
+
super(errors, 'Response validation failed');
|
|
8
|
+
this.name = 'ResponseValidationFailedError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Validate responses created with `aResponse`
|
|
13
|
+
*/ export const validateResponse = (schema)=>{
|
|
14
|
+
const validator = validateWithTypeBox(schema);
|
|
15
|
+
return {
|
|
16
|
+
after: async (req)=>{
|
|
17
|
+
const body = req.response?.body;
|
|
18
|
+
const headers = parseHeaders(req.response.headers);
|
|
19
|
+
const contentType = headers.get('content-type') ?? '';
|
|
20
|
+
if ((body?.length ?? 0) === 0) {
|
|
21
|
+
console.debug(`[validateResponse]`, `Response body is empty`);
|
|
22
|
+
}
|
|
23
|
+
if ((contentType?.includes('application/json') ?? false) === false) {
|
|
24
|
+
console.debug(`[validateResponse]`, `Response body is not JSON`);
|
|
25
|
+
}
|
|
26
|
+
const maybeValid = validator(tryAsJSON(req.response.body));
|
|
27
|
+
if ('errors' in maybeValid) {
|
|
28
|
+
console.error(`[validateResponse]`, `Response validation failed`, req.response.body, formatTypeBoxErrors(maybeValid.errors), schema.title);
|
|
29
|
+
throw new ResponseValidationFailedError(maybeValid.errors);
|
|
30
|
+
}
|
|
31
|
+
console.debug(`[validateResponse]`, `Response is valid`, schema.title);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nrfcloud/lambda-helpers",
|
|
3
|
+
"version": "1.0.28",
|
|
4
|
+
"description": "Helper functions for AWS Lambda functions used in nRF Cloud.",
|
|
5
|
+
"homepage": "https://github.com/nrfcloud/lambda-helpers#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/nrfcloud/lambda-helpers/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/nrfcloud/lambda-helpers.git"
|
|
12
|
+
},
|
|
13
|
+
"author": "Nordic Semiconductor ASA | nordicsemi.no",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --no-warnings --experimental-transform-types --test \"./*.spec.ts\" \"./.npm/*.spec.ts\"",
|
|
16
|
+
"prepublishOnly": "node --experimental-strip-types ./.npm/compile.ts && npx tsc -P ./.npm/tsconfig.npm.json --outDir ./npm"
|
|
17
|
+
},
|
|
18
|
+
"license": "BSD-3-Clause",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./npm/export.d.ts",
|
|
24
|
+
"default": "./npm/export.js"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"npm",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@bifravst/prettier-config": "1.1.17",
|
|
37
|
+
"@nrfcloud/validate-with-typebox": "npm:@jsr/nrfcloud__validate-with-typebox@^1.0.5",
|
|
38
|
+
"@swc/core": "1.15.47",
|
|
39
|
+
"@types/aws-lambda": "8.10.162",
|
|
40
|
+
"@types/node": "25.9.5",
|
|
41
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
42
|
+
"prettier": "3.9.6",
|
|
43
|
+
"typescript": "npm:@typescript/typescript6@6.0.2"
|
|
44
|
+
},
|
|
45
|
+
"prettier": "@bifravst/prettier-config",
|
|
46
|
+
"release": {
|
|
47
|
+
"branches": [
|
|
48
|
+
"main"
|
|
49
|
+
],
|
|
50
|
+
"remoteTags": true,
|
|
51
|
+
"plugins": [
|
|
52
|
+
"@semantic-release/commit-analyzer",
|
|
53
|
+
"@semantic-release/release-notes-generator",
|
|
54
|
+
"@semantic-release/npm",
|
|
55
|
+
[
|
|
56
|
+
"@semantic-release/github",
|
|
57
|
+
{
|
|
58
|
+
"successCommentCondition": false,
|
|
59
|
+
"failCommentCondition": false
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=24.18.1 <25",
|
|
66
|
+
"npm": ">=12.0.2 <13"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@aws-lambda-powertools/metrics": "2.34.0",
|
|
70
|
+
"@middy/core": "7.7.2",
|
|
71
|
+
"@middy/input-output-logger": "7.7.2",
|
|
72
|
+
"@nrfcloud/problem-detail": "npm:@jsr/nrfcloud__problem-detail@1.4.2",
|
|
73
|
+
"@sinclair/typebox": "0.34.52"
|
|
74
|
+
}
|
|
75
|
+
}
|