UNPKG

@baqhub/sdk

Version:

The official JavaScript SDK for the BAQ federated app platform.

357 lines (356 loc) 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = void 0; const tslib_1 = require("tslib"); const es6_url_template_1 = tslib_1.__importDefault(require("es6-url-template")); const compact_js_1 = tslib_1.__importDefault(require("lodash/compact.js")); const constants_js_1 = require("../constants.js"); const async_js_1 = require("../helpers/async.js"); const customError_js_1 = require("../helpers/customError.js"); const headers_js_1 = require("../helpers/headers.js"); const string_js_1 = require("../helpers/string.js"); const time_js_1 = require("../helpers/time.js"); const type_js_1 = require("../helpers/type.js"); const httpCredentialsHeader_js_1 = require("../model/core/httpCredentialsHeader.js"); const httpMethod_js_1 = require("../model/core/httpMethod.js"); const httpBearerSignature_js_1 = require("../model/httpSignature/httpBearerSignature.js"); const httpSignature_js_1 = require("../model/httpSignature/httpSignature.js"); const httpSignatureInput_js_1 = require("../model/httpSignature/httpSignatureInput.js"); const query_js_1 = require("../model/query/query.js"); const queryFilter_js_1 = require("../model/query/queryFilter.js"); const appRecord_js_1 = require("../model/recordTypes/appRecord.js"); const entityRecord_js_1 = require("../model/recordTypes/entityRecord.js"); const record_js_1 = require("../model/records/record.js"); const blobResponse_js_1 = require("../model/response/blobResponse.js"); const recordResponse_js_1 = require("../model/response/recordResponse.js"); const recordsResponse_js_1 = require("../model/response/recordsResponse.js"); const api_js_1 = require("./api.js"); const http_js_1 = require("./http.js"); function fixUrl(url) { if (url.includes("/events")) { return url.replace("https://localhost", "http://localhost:5254"); } if (url.startsWith("https://localhost")) { return url.replace("https://localhost", "http://localhost:5173"); } return url; } function buildClientBase(clientOptions) { const { getEntityRecord: ger, authorizationBuilder, bearerBuilder, } = clientOptions; const getEntityRecord = async_js_1.Async.sharePromise(ger); // // Sync entity record. // let entityRecord; getEntityRecord().then(e => (entityRecord = e), type_js_1.noop); function getEntityRecordSync() { if (!entityRecord) { throw Error("Entity record not available"); } return entityRecord; } // // Template resolution. // function getEntityUrlTemplateForRecord(entityRecord, endpoint) { const firstServer = entityRecord.content.servers[0]; if (!firstServer) { throw new Error("No server found."); } return new es6_url_template_1.default(firstServer.endpoints[endpoint]); } async function getEntityUrlTemplate(endpoint, signal) { const entityRecord = await getEntityRecord(signal); return getEntityUrlTemplateForRecord(entityRecord, endpoint); } async function expandUrlTemplate(endpoint, values, signal) { const urlTemplate = await getEntityUrlTemplate(endpoint, signal); return fixUrl(urlTemplate.expand(values)); } // // Records. // async function getRecord(knownModel, model, entity, recordId, { query, signal } = {}) { const url = await expandUrlTemplate("record", { entity, record_id: recordId, }, signal); const urlAndQuery = url + query_js_1.Query.singleToQueryString(query); const httpOptions = { authorizationBuilder, signal }; const responseModel = recordResponse_js_1.RecordResponse.io(knownModel, model); const [, response] = await api_js_1.Api.get(responseModel, urlAndQuery, httpOptions); return response; } async function getRecordVersion(knownModel, model, entity, recordId, versionHash, { query, signal } = {}) { const url = await expandUrlTemplate("recordVersion", { entity, record_id: recordId, version_hash: versionHash, }, signal); const urlAndQuery = url + query_js_1.Query.singleToQueryString(query); const httpOptions = { authorizationBuilder, signal }; const responseModel = recordResponse_js_1.RecordResponse.io(knownModel, model); const [, response] = await api_js_1.Api.get(responseModel, urlAndQuery, httpOptions); return response; } async function getOwnRecord(knownModel, model, recordId, options) { const entityRecord = await getEntityRecord(options?.signal); return getRecord(knownModel, model, entityRecord.author.entity, recordId, options); } async function getRecords(knownModel, model, query, signal) { const url = await expandUrlTemplate("records", {}, signal); const urlAndQuery = url + query_js_1.Query.toQueryString(query); const httpOptions = { authorizationBuilder, signal }; const responseModel = (0, recordsResponse_js_1.recordsResponse)(knownModel, model); const [, response] = await api_js_1.Api.get(responseModel, urlAndQuery, httpOptions); return response; } async function getMoreRecords(knownModel, model, query, signal) { const url = await expandUrlTemplate("records", {}, signal); const urlAndQuery = url + query; const httpOptions = { authorizationBuilder, signal }; const responseModel = (0, recordsResponse_js_1.recordsResponse)(knownModel, model); const [, response] = await api_js_1.Api.get(responseModel, urlAndQuery, httpOptions); return response; } async function recordEventSource(recordModel, onRecord, query, signal) { try { const url = await expandUrlTemplate("events", {}, signal); const urlAndQuery = url + query_js_1.Query.toQueryString(query); const httpOptions = { authorizationBuilder, signal }; api_js_1.Api.eventSource(recordModel, onRecord, "record", urlAndQuery, httpOptions); } catch (error) { if (error instanceof async_js_1.AbortedError) { return; } throw error; } } async function postRecordBaseAsync(knownModel, recordModel, record, signal, options = {}) { const url = await expandUrlTemplate("newRecord", {}, signal); const responseModel = recordResponse_js_1.RecordResponse.io(knownModel, recordModel); const httpOptions = { ...options, authorizationBuilder, signal }; return await api_js_1.Api.post(responseModel, recordModel, record, url, httpOptions); } async function postRecord(knownModel, recordModel, record, signal) { const [, response] = await postRecordBaseAsync(knownModel, recordModel, record, signal); return response; } async function postAppRecord(record, credentialsRecord, signal) { const credentialsHeader = httpCredentialsHeader_js_1.HttpCredentialsHeader.ofRecord(credentialsRecord); const options = { headers: { [constants_js_1.Constants.credentialsHeader]: httpCredentialsHeader_js_1.HttpCredentialsHeader.toString(credentialsHeader), }, }; const [headers, response] = await postRecordBaseAsync(record_js_1.AnyRecord, appRecord_js_1.AppRecord, record, signal, options); const responseCredentials = httpCredentialsHeader_js_1.HttpCredentialsHeader.tryParseHeader(headers.get(constants_js_1.Constants.credentialsHeader)); if (!responseCredentials) { throw new Error("Server credentials not found."); } return [responseCredentials.publicKey, response.record]; } async function putRecord(knownModel, recordModel, record, signal) { const url = await expandUrlTemplate("record", { entity: record.author.entity, record_id: record.id, }, signal); const responseModel = recordResponse_js_1.RecordResponse.io(knownModel, recordModel); const httpOptions = { authorizationBuilder, signal }; const [, response] = await api_js_1.Api.put(responseModel, recordModel, record, url, httpOptions); return response; } async function deleteRecord(knownModel, record, signal) { const url = await expandUrlTemplate("record", { entity: record.author.entity, record_id: record.id, }, signal); const responseModel = recordResponse_js_1.RecordResponse.io(knownModel, record_js_1.RNoContentRecord); const httpOptions = { authorizationBuilder, signal }; const [_, response] = await api_js_1.Api.delete(responseModel, record_js_1.RNoContentRecord, record, url, httpOptions); return response; } // // Discovery. // async function discover(entity, signal) { const query = query_js_1.Query.new({ pageSize: 1, proxyTo: entity, filter: queryFilter_js_1.Q.and(queryFilter_js_1.Q.author(entity), queryFilter_js_1.Q.type(entityRecord_js_1.EntityRecord)), }); const { records } = await getRecords(record_js_1.AnyRecord, entityRecord_js_1.EntityRecord, query, signal); const firstRecord = records[0]; if (!firstRecord) { throw new customError_js_1.ErrorWithData("Discovery failed", { records }); } return firstRecord; } // // Blobs. // async function uploadBlob(blob, signal) { if (!blob.type) { throw new customError_js_1.ErrorWithData("Blob does not have a type.", { blob }); } const url = await expandUrlTemplate("newBlob", {}, signal); const headers = { "Content-Type": blob.type }; const httpOptions = { authorizationBuilder, headers, signal }; const [, r] = await api_js_1.Api.postBlob(blobResponse_js_1.RBlobResponse, blob, url, httpOptions); return r; } async function downloadBlob(record, blob, signal) { const url = await expandUrlTemplate("recordBlob", { entity: record.author.entity, record_id: record.id, blob_hash: blob.hash, file_name: blob.name, }, signal); const isProxyRecord = record.source === "proxy"; const query = isProxyRecord ? { proxyTo: record.author.entity } : undefined; const options = { authorizationBuilder, signal, query, }; const [, result] = await http_js_1.Http.download(url, options); return result; } function blobUrlBuilderFor(entityRecord) { const urlTemplate = getEntityUrlTemplateForRecord(entityRecord, "recordBlob"); return (record, blob, expiresInSeconds) => { const maybeAddBearer = (url) => { const isProxyRecord = record.source === "proxy"; const bearer = (() => { if (!bearerBuilder || (record_js_1.Record.isPublic(record) && !isProxyRecord)) { return undefined; } const expiresAt = expiresInSeconds ? Date.now() + expiresInSeconds * 1000 : (0, time_js_1.findStableTimestamp)(blob.hash, 90); return bearerBuilder(url, expiresAt); })(); const proxyTo = (() => { if (!isProxyRecord) { return; } return record.author.entity; })(); const query = (0, compact_js_1.default)([ bearer && ["bearer", bearer], proxyTo && ["proxy_to", proxyTo], ]); return url + string_js_1.Str.buildQuery(query); }; const url = fixUrl(urlTemplate.expand({ entity: record.author.entity, record_id: record.id, blob_hash: blob.hash, file_name: blob.name, })); return maybeAddBearer(url); }; } async function blobUrlBuilder() { const entityRecord = await getEntityRecord(); return blobUrlBuilderFor(entityRecord); } return { expandUrlTemplate, getEntityRecord, getEntityRecordSync, getRecord, getRecordVersion, getOwnRecord, getRecords, getMoreRecords, recordEventSource, postRecord, postAppRecord, putRecord, deleteRecord, discover, uploadBlob, downloadBlob, blobUrlBuilderFor, blobUrlBuilder, }; } async function getEntityRecordFromEntityRecordUrl(entityRecordUrl, signal) { const options = { signal }; const responseModel = recordResponse_js_1.RecordResponse.io(record_js_1.AnyRecord, entityRecord_js_1.EntityRecord); const [, { record }] = await api_js_1.Api.get(responseModel, fixUrl(entityRecordUrl), options); return record; } async function getEntityRecordFromEntity(entity, signal) { // Perform discovery. const headers = await http_js_1.Http.head(fixDiscoverUrl(`https://${entity}/`)); const entityRecordLink = (0, headers_js_1.findLink)(headers, "https://baq.dev/rels/entity-record"); if (!entityRecordLink) { throw new Error("Entity record link not found."); } // Fetch the record. return getEntityRecordFromEntityRecordUrl(entityRecordLink, signal); } function buildClientFromUrl(entityRecordUrl) { const getEntityRecord = (signal) => getEntityRecordFromEntityRecordUrl(entityRecordUrl, signal); return buildClientBase({ getEntityRecord }); } function buildClientFromEntity(entity) { const getEntityRecord = (signal) => getEntityRecordFromEntity(entity, signal); return buildClientBase({ getEntityRecord }); } function buildClientFromRecord(entityRecord) { const getEntityRecord = () => Promise.resolve(entityRecord); return buildClientBase({ getEntityRecord }); } function buildAuthenticatedClient(state) { const { entityRecord, appRecord, credentialsRecord, authorizationId } = state; const appRecordId = appRecord.id; const privateKey = credentialsRecord.content.privateKey; const authorizationBuilder = (m, url, headers) => { const input = httpSignatureInput_js_1.HttpSignatureInput.new(m, url, headers, authorizationId); const signature = httpSignature_js_1.HttpSignature.request(appRecordId, privateKey, input); return httpSignature_js_1.HttpSignature.toHeader(signature); }; const bearerBuilder = (url, expiresAt) => { const m = httpMethod_js_1.HttpMethod.GET; const signatureInput = httpSignatureInput_js_1.HttpSignatureInput.new(m, url, {}, authorizationId); const signature = httpBearerSignature_js_1.HttpBearerSignature.request(appRecordId, privateKey, signatureInput, expiresAt); return httpBearerSignature_js_1.HttpBearerSignature.toQuery(signature); }; const getEntityRecord = () => Promise.resolve(entityRecord); return buildClientBase({ getEntityRecord, authorizationBuilder, bearerBuilder, }); } // // Static discovery. // function fixDiscoverUrl(url) { switch (url) { case "https://quentez.localhost/": return "http://localhost:5254/api/quentez.localhost"; case "https://testaccount1.localhost/": return "http://localhost:5254/api/testaccount1.localhost"; case "https://testaccount2.localhost/": return "http://localhost:5254/api/testaccount2.localhost"; default: return url; } } async function discover(entity, signal) { const client = buildClientFromEntity(entity); await client.getEntityRecord(signal); return client; } exports.Client = { ofUrl: buildClientFromUrl, ofEntity: buildClientFromEntity, ofRecord: buildClientFromRecord, authenticated: buildAuthenticatedClient, discover, };