@baqhub/sdk
Version:
The official JavaScript SDK for the BAQ federated app platform.
357 lines (356 loc) • 16 kB
JavaScript
"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,
};