@hello.nrfcloud.com/proto
Version:
Documents the communication protocol between the hello.nrfcloud.com backend and the web application
131 lines (126 loc) • 3.5 kB
text/typescript
import { validateWithTypeBox } from '@hello.nrfcloud.com/proto'
import type { ProblemDetail } from '@hello.nrfcloud.com/proto/hello'
import { Context } from '@hello.nrfcloud.com/proto/hello'
import { type Static, type TObject, type TSchema } from '@sinclair/typebox'
export type TypedFetchResponse<ResponseBodySchemaType extends TSchema> =
| {
error: Static<typeof ProblemDetail>
}
| { result: Static<ResponseBodySchemaType> }
export const typedFetch = <
ResponseBodySchemaType extends TSchema,
RequestBodySchema extends TObject,
>({
responseBodySchema,
requestBodySchema,
fetchImplementation,
}: {
responseBodySchema: ResponseBodySchemaType
requestBodySchema?: RequestBodySchema
fetchImplementation?: typeof fetch
}): ((
url: URL,
body?: Static<RequestBodySchema>,
init?: Omit<RequestInit, 'body'>,
) => Promise<TypedFetchResponse<ResponseBodySchemaType>>) => {
const validateResponseBody = validateWithTypeBox(responseBodySchema)
const validateRequestBody =
requestBodySchema !== undefined
? validateWithTypeBox(requestBodySchema)
: undefined
return async (url, body, init) => {
if (body !== undefined && validateRequestBody !== undefined) {
const maybeValidRequestBody = validateRequestBody(body)
if ('errors' in maybeValidRequestBody) {
return {
error: {
'@context': Context.problemDetail.toString(),
title: 'Request body validation failed',
detail: JSON.stringify({
body,
errors: maybeValidRequestBody.errors,
}),
},
}
}
}
let res: Response | undefined = undefined
try {
const hasBody = body !== undefined
const headers = new Headers(init?.headers)
if (hasBody)
headers.set('content-type', 'application/json; charset=utf-8')
res = await (fetchImplementation ?? fetch)(url, {
...(init ?? {}),
method: init?.method ?? (hasBody ? 'POST' : 'GET'),
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
})
} catch (err) {
return {
error: {
'@context': Context.problemDetail.toString(),
title: (err as Error).message,
},
}
}
const hasContent =
parseInt(res.headers.get('content-length') ?? '0', 10) > 0
if (!res.ok) {
if (
res.headers.get('content-type')?.includes('application/problem+json') ??
false
) {
return {
error: await res.json(),
}
}
return {
error: {
'@context': Context.problemDetail.toString(),
title: `Request failed (${res.status})`,
detail: JSON.stringify({
body: hasContent ? await res.text() : undefined,
}),
},
}
}
let responseBody = undefined
if (hasContent) {
responseBody = await res.text()
const isJSON =
res.headers.get('content-type')?.includes('application/json') ?? false
if (isJSON) {
try {
responseBody = JSON.parse(responseBody)
} catch {
return {
error: {
'@context': Context.problemDetail.toString(),
title: `Failed to parse response as JSON!`,
detail: JSON.stringify({
body: responseBody,
}),
},
}
}
}
}
const maybeValidResponseBody = validateResponseBody(responseBody)
if ('errors' in maybeValidResponseBody) {
return {
error: {
'@context': Context.problemDetail.toString(),
title: 'Response body validation failed',
detail: JSON.stringify({
body: responseBody,
errors: maybeValidResponseBody.errors,
}),
},
}
}
return {
result: responseBody,
}
}
}