370 lines
14 KiB
JavaScript
370 lines
14 KiB
JavaScript
const runtimeGlobal = globalThis;
|
|
const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/;
|
|
const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
|
const MAX_CURSOR_LENGTH = 1024;
|
|
const MAX_ERROR_MESSAGE_LENGTH = 256;
|
|
const MAX_RETRY_AFTER_SECONDS = 86400;
|
|
const ERROR_STATUS = {
|
|
runtime_unavailable: 503,
|
|
upstream_invalid_response: 502,
|
|
invalid_request: [400, 422],
|
|
invalid_content_type: 415,
|
|
invalid_revision: 422,
|
|
invalid_cursor: 400,
|
|
cursor_expired: 410,
|
|
invalid_collection_name: 422,
|
|
invalid_document_id: 422,
|
|
invalid_document_data: 422,
|
|
invalid_project_id: 422,
|
|
authentication_required: 401,
|
|
origin_not_allowed: 403,
|
|
method_not_allowed: 405,
|
|
route_not_found: 404,
|
|
instance_not_found: 404,
|
|
collection_not_found: 404,
|
|
document_not_found: 404,
|
|
revision_conflict: 409,
|
|
document_too_large: 413,
|
|
quota_exceeded: 409,
|
|
rate_limited: 429,
|
|
data_service_unavailable: 503,
|
|
request_too_large: 413
|
|
};
|
|
const ERROR_MESSAGE = {
|
|
runtime_unavailable: "Preview data runtime is unavailable",
|
|
upstream_invalid_response: "Data Service returned an invalid response",
|
|
invalid_request: "Data Service request is invalid",
|
|
invalid_content_type: "Data Service request content type is invalid",
|
|
invalid_revision: "Data Service document revision is invalid",
|
|
invalid_cursor: "Data Service cursor is invalid",
|
|
cursor_expired: "Data Service cursor has expired",
|
|
invalid_collection_name: "Data Service collection is invalid",
|
|
invalid_document_id: "Data Service document ID is invalid",
|
|
invalid_document_data: "Data Service document data is invalid",
|
|
invalid_project_id: "Data Service project ID is invalid",
|
|
authentication_required: "Preview data authorization is required",
|
|
origin_not_allowed: "Preview data Origin is not allowed",
|
|
method_not_allowed: "Preview data method is not allowed",
|
|
route_not_found: "Preview data route was not found",
|
|
instance_not_found: "Data Service project instance was not found",
|
|
collection_not_found: "Data Service collection was not found",
|
|
document_not_found: "Data Service document was not found",
|
|
revision_conflict: "Data Service document revision conflicts",
|
|
document_too_large: "Data Service document is too large",
|
|
quota_exceeded: "Data Service quota exceeded",
|
|
rate_limited: "Preview data request rate limit exceeded",
|
|
data_service_unavailable: "Data Service is temporarily unavailable",
|
|
request_too_large: "Preview data request is too large"
|
|
};
|
|
const CONTEXT_KEYS = /* @__PURE__ */ new Set([
|
|
"resource",
|
|
"limit",
|
|
"current",
|
|
"attempted",
|
|
"actual",
|
|
"allowed",
|
|
"current_revision",
|
|
"retry_after_seconds"
|
|
]);
|
|
const CONTEXT_RESOURCES = /* @__PURE__ */ new Set(["instances", "collections", "documents", "bytes"]);
|
|
function isRecord(value) {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
function positiveInteger(value) {
|
|
return Number.isSafeInteger(value) && value > 0;
|
|
}
|
|
function boundedText(value, maximum) {
|
|
return typeof value === "string" && value.length > 0 && value.length <= maximum;
|
|
}
|
|
function boundedTimestamp(value) {
|
|
return boundedText(value, 64) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/.test(value) && Number.isFinite(Date.parse(value));
|
|
}
|
|
function hasOnlyKeys(value, required, optional = []) {
|
|
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => allowed.has(key));
|
|
}
|
|
function statusMatches(code, status) {
|
|
const expected = ERROR_STATUS[code];
|
|
return Array.isArray(expected) ? expected.includes(status) : expected === status;
|
|
}
|
|
function frozenContext(value) {
|
|
return value === void 0 ? void 0 : Object.freeze({ ...value });
|
|
}
|
|
class DataServiceError extends Error {
|
|
code;
|
|
status;
|
|
retryable;
|
|
retryAfterSeconds;
|
|
context;
|
|
constructor(code, status, retryable, retryAfterSeconds, context) {
|
|
super(ERROR_MESSAGE[code]);
|
|
Object.defineProperty(this, "name", { value: "DataServiceError", enumerable: false });
|
|
this.code = code;
|
|
this.status = status;
|
|
this.retryable = retryable;
|
|
if (retryAfterSeconds !== void 0) this.retryAfterSeconds = retryAfterSeconds;
|
|
const safeContext = frozenContext(context);
|
|
if (safeContext !== void 0) this.context = safeContext;
|
|
Object.freeze(this);
|
|
}
|
|
}
|
|
function runtimeUnavailable() {
|
|
return new DataServiceError("runtime_unavailable", 503, true);
|
|
}
|
|
function invalidRequest() {
|
|
return new DataServiceError("invalid_request", 422, false);
|
|
}
|
|
function upstreamInvalidResponse() {
|
|
return new DataServiceError("upstream_invalid_response", 502, false);
|
|
}
|
|
function readRuntime() {
|
|
const candidate = globalThis.__MAKELORE_DATA__;
|
|
if (!isRecord(candidate) || candidate.contractVersion !== 1 || !boundedText(candidate.token, 256) || !/^[A-Za-z0-9_-]+$/.test(candidate.token) || !boundedText(candidate.endpoint, 256)) {
|
|
throw runtimeUnavailable();
|
|
}
|
|
let endpoint;
|
|
try {
|
|
endpoint = new URL(candidate.endpoint);
|
|
} catch {
|
|
throw runtimeUnavailable();
|
|
}
|
|
if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port || endpoint.username || endpoint.password || endpoint.pathname !== "/api/runtime/data/v1" || endpoint.search || endpoint.hash) {
|
|
throw runtimeUnavailable();
|
|
}
|
|
return {
|
|
endpoint: `${endpoint.origin}${endpoint.pathname}`,
|
|
token: candidate.token,
|
|
contractVersion: 1
|
|
};
|
|
}
|
|
function requireCollection(value) {
|
|
if (typeof value !== "string" || !COLLECTION_PATTERN.test(value)) throw invalidRequest();
|
|
return value;
|
|
}
|
|
function requireDocumentId(value) {
|
|
if (typeof value !== "string" || value === "." || value === ".." || !DOCUMENT_ID_PATTERN.test(value)) {
|
|
throw invalidRequest();
|
|
}
|
|
return value;
|
|
}
|
|
function requireData(value) {
|
|
if (!isRecord(value)) throw invalidRequest();
|
|
try {
|
|
if (JSON.stringify(value) === void 0) throw new Error("data is not JSON");
|
|
} catch {
|
|
throw invalidRequest();
|
|
}
|
|
return value;
|
|
}
|
|
function requireOptions(value, allowed) {
|
|
if (value === void 0) return {};
|
|
if (!isRecord(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw invalidRequest();
|
|
return value;
|
|
}
|
|
function readIfRevision(value) {
|
|
if (value === void 0) return void 0;
|
|
if (!positiveInteger(value)) throw invalidRequest();
|
|
return value;
|
|
}
|
|
function buildDocumentPath(endpoint, collection, documentId) {
|
|
return `${endpoint}/collections/${encodeURIComponent(collection)}/documents/${encodeURIComponent(documentId)}`;
|
|
}
|
|
function parseContext(value) {
|
|
if (value === void 0) return void 0;
|
|
if (!isRecord(value)) throw upstreamInvalidResponse();
|
|
const context = {};
|
|
for (const [key, item] of Object.entries(value)) {
|
|
if (!CONTEXT_KEYS.has(key)) throw upstreamInvalidResponse();
|
|
if (key === "resource") {
|
|
if (!boundedText(item, 16) || !CONTEXT_RESOURCES.has(item)) throw upstreamInvalidResponse();
|
|
context.resource = item;
|
|
} else {
|
|
if (!positiveInteger(item) && item !== 0) throw upstreamInvalidResponse();
|
|
context[key] = item;
|
|
}
|
|
}
|
|
return Object.keys(context).length === 0 ? void 0 : context;
|
|
}
|
|
function readRetryAfter(response) {
|
|
const raw = response.headers?.get("retry-after")?.trim();
|
|
if (!raw || !/^\d+$/.test(raw)) return void 0;
|
|
const seconds = Number(raw);
|
|
return Number.isSafeInteger(seconds) && seconds <= MAX_RETRY_AFTER_SECONDS ? seconds : void 0;
|
|
}
|
|
async function parseError(response) {
|
|
try {
|
|
const payload = await response.json();
|
|
if (!isRecord(payload) || !isRecord(payload.detail)) throw upstreamInvalidResponse();
|
|
const detail = payload.detail;
|
|
if (!hasOnlyKeys(detail, ["code", "message", "retryable"], ["context"]) || !boundedText(detail.code, 64) || !Object.prototype.hasOwnProperty.call(ERROR_STATUS, detail.code) || !boundedText(detail.message, MAX_ERROR_MESSAGE_LENGTH) || typeof detail.retryable !== "boolean" || !statusMatches(detail.code, response.status)) {
|
|
throw upstreamInvalidResponse();
|
|
}
|
|
const context = parseContext(detail.context);
|
|
return new DataServiceError(
|
|
detail.code,
|
|
response.status,
|
|
detail.retryable,
|
|
readRetryAfter(response),
|
|
context
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof DataServiceError) throw error;
|
|
throw upstreamInvalidResponse();
|
|
}
|
|
}
|
|
async function parseJson(response) {
|
|
try {
|
|
return await response.json();
|
|
} catch {
|
|
throw upstreamInvalidResponse();
|
|
}
|
|
}
|
|
function parseDocument(value) {
|
|
if (!isRecord(value) || !hasOnlyKeys(value, ["id", "data", "revision", "created_at", "updated_at"]) || typeof value.id !== "string" || value.id === "." || value.id === ".." || !DOCUMENT_ID_PATTERN.test(value.id) || !isRecord(value.data) || !positiveInteger(value.revision) || !boundedTimestamp(value.created_at) || !boundedTimestamp(value.updated_at)) {
|
|
throw upstreamInvalidResponse();
|
|
}
|
|
return {
|
|
id: value.id,
|
|
data: value.data,
|
|
revision: value.revision,
|
|
created_at: value.created_at,
|
|
updated_at: value.updated_at
|
|
};
|
|
}
|
|
function parsePage(value) {
|
|
if (!isRecord(value) || !hasOnlyKeys(value, ["items", "next_cursor", "limit"]) || !Array.isArray(value.items) || value.items.length > 100 || !positiveInteger(value.limit) || value.limit > 100 || value.next_cursor !== null && !boundedText(value.next_cursor, MAX_CURSOR_LENGTH)) {
|
|
throw upstreamInvalidResponse();
|
|
}
|
|
const items = value.items.map(parseDocument);
|
|
if (new Set(items.map((item) => item.id)).size !== items.length) throw upstreamInvalidResponse();
|
|
return {
|
|
items,
|
|
next_cursor: value.next_cursor === null ? null : value.next_cursor,
|
|
limit: value.limit
|
|
};
|
|
}
|
|
async function request(method, url, expectedStatus, parse, body, ifRevision) {
|
|
const runtime = readRuntime();
|
|
const fetchImpl = runtimeGlobal.fetch;
|
|
if (typeof fetchImpl !== "function") throw runtimeUnavailable();
|
|
const headers = {
|
|
Accept: "application/json",
|
|
Authorization: `Bearer ${runtime.token}`
|
|
};
|
|
const serializedBody = body === void 0 ? void 0 : (() => {
|
|
try {
|
|
const encoded = JSON.stringify(body);
|
|
return encoded === void 0 ? null : encoded;
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
if (serializedBody === null) throw invalidRequest();
|
|
if (serializedBody !== void 0) headers["Content-Type"] = "application/json";
|
|
if (ifRevision !== void 0) headers["If-Match"] = `"${ifRevision}"`;
|
|
let response;
|
|
try {
|
|
response = await fetchImpl(url, {
|
|
method,
|
|
headers,
|
|
...serializedBody === void 0 ? {} : { body: serializedBody }
|
|
});
|
|
} catch {
|
|
throw runtimeUnavailable();
|
|
}
|
|
if (!response || !Number.isSafeInteger(response.status)) throw upstreamInvalidResponse();
|
|
if (response.status !== expectedStatus) throw await parseError(response);
|
|
if (expectedStatus === 204) return void 0;
|
|
return parse(await parseJson(response));
|
|
}
|
|
function listPath(endpoint, collection, options) {
|
|
const params = new URLSearchParams();
|
|
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
if (options.cursor !== void 0) params.set("cursor", options.cursor);
|
|
const query = params.toString();
|
|
return `${endpoint}/collections/${encodeURIComponent(collection)}/documents${query ? `?${query}` : ""}`;
|
|
}
|
|
async function getDocument(collection, documentId) {
|
|
const runtime = readRuntime();
|
|
const safeCollection = requireCollection(collection);
|
|
const safeDocumentId = requireDocumentId(documentId);
|
|
return await request(
|
|
"GET",
|
|
buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId),
|
|
200,
|
|
parseDocument
|
|
);
|
|
}
|
|
async function listDocuments(collection, input = {}) {
|
|
const runtime = readRuntime();
|
|
const safeCollection = requireCollection(collection);
|
|
const options = requireOptions(input, ["limit", "cursor"]);
|
|
const limit = options.limit;
|
|
if (limit !== void 0 && (!positiveInteger(limit) || limit > 100)) throw invalidRequest();
|
|
const cursor = options.cursor;
|
|
if (cursor !== void 0 && !boundedText(cursor, MAX_CURSOR_LENGTH)) throw invalidRequest();
|
|
return await request(
|
|
"GET",
|
|
listPath(runtime.endpoint, safeCollection, {
|
|
...limit === void 0 ? {} : { limit },
|
|
...cursor === void 0 ? {} : { cursor }
|
|
}),
|
|
200,
|
|
parsePage
|
|
);
|
|
}
|
|
async function putDocument(collection, documentId, value, input = {}) {
|
|
const runtime = readRuntime();
|
|
const safeCollection = requireCollection(collection);
|
|
const safeDocumentId = requireDocumentId(documentId);
|
|
const data2 = requireData(value);
|
|
const options = requireOptions(input, ["ifRevision"]);
|
|
const ifRevision = readIfRevision(options.ifRevision);
|
|
return await request(
|
|
"PUT",
|
|
buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId),
|
|
200,
|
|
parseDocument,
|
|
{ data: data2 },
|
|
ifRevision
|
|
);
|
|
}
|
|
async function deleteDocument(collection, documentId, input = {}) {
|
|
const runtime = readRuntime();
|
|
const safeCollection = requireCollection(collection);
|
|
const safeDocumentId = requireDocumentId(documentId);
|
|
const options = requireOptions(input, ["ifRevision"]);
|
|
const ifRevision = readIfRevision(options.ifRevision);
|
|
await request(
|
|
"DELETE",
|
|
buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId),
|
|
204,
|
|
() => void 0,
|
|
void 0,
|
|
ifRevision
|
|
);
|
|
}
|
|
async function addDocument(collection, value) {
|
|
readRuntime();
|
|
requireCollection(collection);
|
|
requireData(value);
|
|
const randomUuid = runtimeGlobal.crypto?.randomUUID;
|
|
if (typeof randomUuid !== "function") throw runtimeUnavailable();
|
|
const id = randomUuid();
|
|
if (!boundedText(id, 128) || !DOCUMENT_ID_PATTERN.test(id)) throw runtimeUnavailable();
|
|
return await putDocument(collection, id, value);
|
|
}
|
|
const data = Object.freeze({
|
|
get: getDocument,
|
|
list: listDocuments,
|
|
put: putDocument,
|
|
delete: deleteDocument,
|
|
add: addDocument
|
|
});
|
|
var makelore_data_default = data;
|
|
export {
|
|
DataServiceError,
|
|
data,
|
|
makelore_data_default as default
|
|
};
|