@platformatic/client
Advanced tools
Comparing version 0.32.0 to 0.33.0
166
index.js
@@ -8,2 +8,3 @@ 'use strict' | ||
const kGetHeaders = Symbol('getHeaders') | ||
const kTelemetryContext = Symbol('telemetry-context') | ||
const abstractLogging = require('abstract-logging') | ||
@@ -24,3 +25,3 @@ | ||
async function buildOpenAPIClient (options) { | ||
async function buildOpenAPIClient (options, openTelemetry) { | ||
const client = {} | ||
@@ -31,15 +32,16 @@ let spec | ||
// this is tested, not sure why c8 is not picking it up | ||
if (!options.url) { | ||
throw new Error('options.url is required') | ||
} | ||
if (options.path) { | ||
spec = JSON.parse(await fs.readFile(options.path, 'utf8')) | ||
baseUrl = options.url.replace(/\/$/, '') | ||
} else if (options.url) { | ||
} else { | ||
const res = await request(options.url) | ||
spec = await res.body.json() | ||
baseUrl = computeURLWithoutPath(options.url) | ||
} else { | ||
throw new Error('options.url or options.file are required') | ||
} | ||
client[kHeaders] = options.headers || {} | ||
const { fullResponse, throwOnError } = options | ||
let { fullResponse, throwOnError } = options | ||
@@ -52,4 +54,11 @@ for (const path of Object.keys(spec.paths)) { | ||
const operationId = generateOperationId(path, method, methodMeta) | ||
client[operationId] = buildCallFunction(baseUrl, path, method, methodMeta, fullResponse, throwOnError) | ||
const responses = pathMeta[method].responses | ||
const successResponses = Object.entries(responses).filter(([s]) => s.startsWith('2')) | ||
if (successResponses.length !== 1) { | ||
// force fullResponse = true if | ||
// - there is more than 1 responses with 2XX code | ||
// - there is no responses with 2XX code | ||
fullResponse = true | ||
} | ||
client[operationId] = buildCallFunction(baseUrl, path, method, methodMeta, fullResponse, throwOnError, openTelemetry) | ||
} | ||
@@ -67,3 +76,3 @@ } | ||
function buildCallFunction (baseUrl, path, method, methodMeta, fullResponse, throwOnError) { | ||
function buildCallFunction (baseUrl, path, method, methodMeta, fullResponse, throwOnError, openTelemetry) { | ||
const url = new URL(baseUrl) | ||
@@ -79,5 +88,9 @@ method = method.toUpperCase() | ||
let headers = this[kHeaders] | ||
let telemetryContext = null | ||
if (this[kGetHeaders]) { | ||
headers = { ...headers, ...(await this[kGetHeaders]()) } | ||
} | ||
if (this[kTelemetryContext]) { | ||
telemetryContext = this[kTelemetryContext] | ||
} | ||
const body = { ...args } // shallow copy | ||
@@ -112,25 +125,39 @@ const urlToCall = new URL(url) | ||
const res = await request(urlToCall, { | ||
method, | ||
headers: { | ||
...headers, | ||
'content-type': 'application/json; charset=utf-8' | ||
}, | ||
body: JSON.stringify(body), | ||
throwOnError | ||
}) | ||
const { span, telemetryHeaders } = openTelemetry?.startSpanClient(urlToCall.toString(), method, telemetryContext) || { span: null, telemetryHeaders: {} } | ||
let res | ||
try { | ||
res = await request(urlToCall, { | ||
method, | ||
headers: { | ||
...headers, | ||
...telemetryHeaders, | ||
'content-type': 'application/json; charset=utf-8' | ||
}, | ||
body: JSON.stringify(body), | ||
throwOnError | ||
}) | ||
let responseBody | ||
try { | ||
responseBody = res.statusCode === 204 | ||
? await res.body.dump() | ||
: await res.body.json() | ||
} catch (err) { | ||
// maybe the response is a 302, 301, or anything with empty payload | ||
responseBody = {} | ||
} | ||
if (fullResponse) { | ||
return { | ||
statusCode: res.statusCode, | ||
headers: res.headers, | ||
body: responseBody | ||
} | ||
} | ||
const responseBody = res.statusCode === 204 | ||
? await res.body.dump() | ||
: await res.body.json() | ||
if (fullResponse) { | ||
return { | ||
statusCode: res.statusCode, | ||
headers: res.headers, | ||
body: responseBody | ||
} | ||
return responseBody | ||
} catch (err) { | ||
openTelemetry?.setErrorInSpanClient(span, err) | ||
throw err | ||
} finally { | ||
openTelemetry?.endSpanClient(span, res) | ||
} | ||
return responseBody | ||
} | ||
@@ -144,39 +171,51 @@ } | ||
// TODO: For some unknown reason c8 is not picking up the coverage for this function | ||
async function graphql (url, log, headers, query, variables) { | ||
const res = await request(url, { | ||
method: 'POST', | ||
headers: { | ||
...headers, | ||
'content-type': 'application/json; charset=utf-8' | ||
}, | ||
body: JSON.stringify({ | ||
query, | ||
variables | ||
async function graphql (url, log, headers, query, variables, openTelemetry, telemetryContext) { | ||
const { span, telemetryHeaders } = openTelemetry?.startSpanClient(url.toString(), 'POST', telemetryContext) || { span: null, telemetryHeaders: {} } | ||
let res | ||
try { | ||
res = await request(url, { | ||
method: 'POST', | ||
headers: { | ||
...headers, | ||
...telemetryHeaders, | ||
'content-type': 'application/json; charset=utf-8' | ||
}, | ||
body: JSON.stringify({ | ||
query, | ||
variables | ||
}) | ||
}) | ||
}) | ||
const json = await res.body.json() | ||
if (res.statusCode !== 200) { | ||
log.warn({ statusCode: res.statusCode, json }, 'request to client failed') | ||
throw new Error('request to client failed') | ||
} | ||
const json = await res.body.json() | ||
if (json.errors) { | ||
log.warn({ errors: json.errors }, 'errors in graphql response') | ||
const e = new Error(json.errors.map(e => e.message).join('')) | ||
e.errors = json.errors | ||
throw e | ||
} | ||
if (res.statusCode !== 200) { | ||
log.warn({ statusCode: res.statusCode, json }, 'request to client failed') | ||
throw new Error('request to client failed') | ||
} | ||
const keys = Object.keys(json.data) | ||
if (keys.length !== 1) { | ||
return json.data | ||
} else { | ||
return json.data[keys[0]] | ||
if (json.errors) { | ||
log.warn({ errors: json.errors }, 'errors in graphql response') | ||
const e = new Error(json.errors.map(e => e.message).join('')) | ||
e.errors = json.errors | ||
throw e | ||
} | ||
const keys = Object.keys(json.data) | ||
if (keys.length !== 1) { | ||
return json.data | ||
} else { | ||
return json.data[keys[0]] | ||
} | ||
} catch (err) { | ||
openTelemetry?.setErrorInSpanClient(span, err) | ||
throw err | ||
} finally { | ||
openTelemetry?.endSpanClient(span, res) | ||
} | ||
} | ||
function wrapGraphQLClient (url, logger) { | ||
function wrapGraphQLClient (url, openTelemetry, logger) { | ||
return async function ({ query, variables }) { | ||
let headers = this[kHeaders] | ||
const telemetryContext = this[kTelemetryContext] | ||
if (typeof this[kGetHeaders] === 'function') { | ||
@@ -187,7 +226,7 @@ headers = { ...headers, ...(await this[kGetHeaders]()) } | ||
return graphql(url, log, headers, query, variables) | ||
return graphql(url, log, headers, query, variables, openTelemetry, telemetryContext) | ||
} | ||
} | ||
async function buildGraphQLClient (options, logger = abstractLogging) { | ||
async function buildGraphQLClient (options, openTelemetry, logger = abstractLogging) { | ||
options = options || {} | ||
@@ -199,3 +238,3 @@ if (!options.url) { | ||
return { | ||
graphql: wrapGraphQLClient(options.url, logger), | ||
graphql: wrapGraphQLClient(options.url, openTelemetry, logger), | ||
[kHeaders]: options.headers || {} | ||
@@ -220,5 +259,5 @@ } | ||
if (opts.type === 'openapi') { | ||
client = await buildOpenAPIClient(opts) | ||
client = await buildOpenAPIClient(opts, app.openTelemetry) | ||
} else if (opts.type === 'graphql') { | ||
client = await buildGraphQLClient(opts, app.log) | ||
client = await buildGraphQLClient(opts, app.openTelemetry, app.log) | ||
} else { | ||
@@ -245,2 +284,5 @@ throw new Error('opts.type must be either "openapi" or "graphql" ff') | ||
} | ||
if (req.span) { | ||
newClient[kTelemetryContext] = req.span.context | ||
} | ||
req[name] = newClient | ||
@@ -247,0 +289,0 @@ }) |
{ | ||
"name": "@platformatic/client", | ||
"version": "0.32.0", | ||
"version": "0.33.0", | ||
"description": "A client for all platformatic backends", | ||
@@ -24,3 +24,4 @@ "main": "index.js", | ||
"tap": "^16.3.6", | ||
"typescript": "^5.1.3" | ||
"typescript": "^5.1.3", | ||
"@platformatic/telemetry": "0.33.0" | ||
}, | ||
@@ -27,0 +28,0 @@ "dependencies": { |
@@ -14,2 +14,5 @@ 'use strict' | ||
await rejects(buildOpenAPIClient({})) | ||
await rejects(buildOpenAPIClient({ | ||
path: join(__dirname, 'fixtures', 'movies', 'openapi.json') | ||
})) | ||
}) | ||
@@ -525,1 +528,31 @@ | ||
}) | ||
test('302', async ({ teardown, same, rejects }) => { | ||
try { | ||
await fs.unlink(join(__dirname, 'fixtures', 'movies-no-200', 'db.sqlite')) | ||
} catch { | ||
// noop | ||
} | ||
const app = await buildServer(join(__dirname, 'fixtures', 'movies-no-200', 'platformatic.db.json')) | ||
teardown(async () => { | ||
await app.close() | ||
}) | ||
await app.start() | ||
const client = await buildOpenAPIClient({ | ||
url: `${app.url}/`, | ||
path: join(__dirname, 'fixtures', 'movies-no-200', 'openapi.json') | ||
}) | ||
{ | ||
const resp = await client.redirectMe() | ||
same(resp.statusCode, 302) | ||
same(resp.headers.location, 'https://google.com') | ||
} | ||
{ | ||
const resp = await client.nonStandard() | ||
same(resp.statusCode, 470) | ||
console.log(resp) | ||
} | ||
}) |
131370
39
4794
9
6