Files
makelore/resources/coding-skills/data-service/assets/makelore-data.ts

551 lines
18 KiB
TypeScript

export type DataServiceDocument<T extends Record<string, unknown> = Record<string, unknown>> = {
id: string;
data: T;
revision: number;
created_at: string;
updated_at: string;
};
export type DataServiceDocumentPage<T extends Record<string, unknown> = Record<string, unknown>> = {
items: Array<DataServiceDocument<T>>;
next_cursor: string | null;
limit: number;
};
export type DataServiceListOptions = {
limit?: number;
cursor?: string;
};
export type DataServiceWriteOptions = {
ifRevision?: number;
};
export type DataServiceErrorContext = {
resource?: 'instances' | 'collections' | 'documents' | 'bytes';
limit?: number;
current?: number;
attempted?: number;
actual?: number;
allowed?: number;
current_revision?: number;
retry_after_seconds?: number;
};
export type DataServiceErrorCode =
| 'runtime_unavailable'
| 'upstream_invalid_response'
| 'invalid_request'
| 'invalid_content_type'
| 'invalid_revision'
| 'invalid_cursor'
| 'cursor_expired'
| 'invalid_collection_name'
| 'invalid_document_id'
| 'invalid_document_data'
| 'invalid_project_id'
| 'authentication_required'
| 'origin_not_allowed'
| 'method_not_allowed'
| 'route_not_found'
| 'instance_not_found'
| 'collection_not_found'
| 'document_not_found'
| 'revision_conflict'
| 'document_too_large'
| 'quota_exceeded'
| 'rate_limited'
| 'data_service_unavailable'
| 'request_too_large';
declare global {
var __MAKELORE_DATA__: unknown;
}
type FetchResponse = {
status: number;
headers?: { get(name: string): string | null };
json(): Promise<unknown>;
};
type FetchOptions = {
method: 'GET' | 'PUT' | 'DELETE';
headers: Record<string, string>;
body?: string;
};
type RuntimeBinding = {
endpoint: string;
token: string;
contractVersion: 1;
};
type RuntimeGlobal = typeof globalThis & {
__MAKELORE_DATA__?: unknown;
fetch?: (input: string, init: FetchOptions) => Promise<FetchResponse>;
crypto?: { randomUUID?: () => string };
};
const runtimeGlobal = globalThis as RuntimeGlobal;
const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/;
const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
const MAX_CURSOR_LENGTH = 1_024;
const MAX_ERROR_MESSAGE_LENGTH = 256;
const MAX_RETRY_AFTER_SECONDS = 86_400;
const ERROR_STATUS: Readonly<Record<DataServiceErrorCode, number | readonly number[]>> = {
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: Readonly<Record<DataServiceErrorCode, string>> = {
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 = new Set([
'resource',
'limit',
'current',
'attempted',
'actual',
'allowed',
'current_revision',
'retry_after_seconds',
]);
const CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function positiveInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) > 0;
}
function boundedText(value: unknown, maximum: number): value is string {
return typeof value === 'string' && value.length > 0 && value.length <= maximum;
}
function boundedTimestamp(value: unknown): value is string {
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: Record<string, unknown>, required: readonly string[], optional: readonly string[] = []): boolean {
const allowed = 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: DataServiceErrorCode, status: number): boolean {
const expected = ERROR_STATUS[code];
return Array.isArray(expected) ? expected.includes(status) : expected === status;
}
function frozenContext(value: DataServiceErrorContext | undefined): DataServiceErrorContext | undefined {
return value === undefined ? undefined : Object.freeze({ ...value });
}
export class DataServiceError extends Error {
readonly code: DataServiceErrorCode;
readonly status: number;
readonly retryable: boolean;
readonly retryAfterSeconds?: number;
readonly context?: DataServiceErrorContext;
constructor(
code: DataServiceErrorCode,
status: number,
retryable: boolean,
retryAfterSeconds?: number,
context?: DataServiceErrorContext,
) {
super(ERROR_MESSAGE[code]);
Object.defineProperty(this, 'name', { value: 'DataServiceError', enumerable: false });
this.code = code;
this.status = status;
this.retryable = retryable;
if (retryAfterSeconds !== undefined) this.retryAfterSeconds = retryAfterSeconds;
const safeContext = frozenContext(context);
if (safeContext !== undefined) this.context = safeContext;
Object.freeze(this);
}
}
function runtimeUnavailable(): DataServiceError {
return new DataServiceError('runtime_unavailable', 503, true);
}
function invalidRequest(): DataServiceError {
return new DataServiceError('invalid_request', 422, false);
}
function upstreamInvalidResponse(): DataServiceError {
return new DataServiceError('upstream_invalid_response', 502, false);
}
function readRuntime(): RuntimeBinding {
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: URL;
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: string): string {
if (typeof value !== 'string' || !COLLECTION_PATTERN.test(value)) throw invalidRequest();
return value;
}
function requireDocumentId(value: string): string {
if (typeof value !== 'string' || value === '.' || value === '..' || !DOCUMENT_ID_PATTERN.test(value)) {
throw invalidRequest();
}
return value;
}
function requireData(value: Record<string, unknown>): Record<string, unknown> {
if (!isRecord(value)) throw invalidRequest();
try {
if (JSON.stringify(value) === undefined) throw new Error('data is not JSON');
} catch {
throw invalidRequest();
}
return value;
}
function requireOptions(value: unknown, allowed: string[]): Record<string, unknown> {
if (value === undefined) return {};
if (!isRecord(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw invalidRequest();
return value;
}
function readIfRevision(value: unknown): number | undefined {
if (value === undefined) return undefined;
if (!positiveInteger(value)) throw invalidRequest();
return value;
}
function buildDocumentPath(endpoint: string, collection: string, documentId: string): string {
return `${endpoint}/collections/${encodeURIComponent(collection)}/documents/${encodeURIComponent(documentId)}`;
}
function parseContext(value: unknown): DataServiceErrorContext | undefined {
if (value === undefined) return undefined;
if (!isRecord(value)) throw upstreamInvalidResponse();
const context: DataServiceErrorContext = {};
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 as DataServiceErrorContext['resource'];
} else {
if (!positiveInteger(item) && item !== 0) throw upstreamInvalidResponse();
context[key as Exclude<keyof DataServiceErrorContext, 'resource'>] = item as never;
}
}
return Object.keys(context).length === 0 ? undefined : context;
}
function readRetryAfter(response: FetchResponse): number | undefined {
const raw = response.headers?.get('retry-after')?.trim();
if (!raw || !/^\d+$/.test(raw)) return undefined;
const seconds = Number(raw);
return Number.isSafeInteger(seconds) && seconds <= MAX_RETRY_AFTER_SECONDS ? seconds : undefined;
}
async function parseError(response: FetchResponse): Promise<DataServiceError> {
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 as DataServiceErrorCode, response.status)) {
throw upstreamInvalidResponse();
}
const context = parseContext(detail.context);
return new DataServiceError(
detail.code as DataServiceErrorCode,
response.status,
detail.retryable,
readRetryAfter(response),
context,
);
} catch (error: unknown) {
if (error instanceof DataServiceError) throw error;
throw upstreamInvalidResponse();
}
}
async function parseJson(response: FetchResponse): Promise<unknown> {
try {
return await response.json();
} catch {
throw upstreamInvalidResponse();
}
}
function parseDocument(value: unknown): DataServiceDocument {
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: unknown): DataServiceDocumentPage {
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<T>(
method: FetchOptions['method'],
url: string,
expectedStatus: number,
parse: (value: unknown) => T,
body?: Record<string, unknown>,
ifRevision?: number,
): Promise<T> {
const runtime = readRuntime();
const fetchImpl = runtimeGlobal.fetch;
if (typeof fetchImpl !== 'function') throw runtimeUnavailable();
const headers: Record<string, string> = {
Accept: 'application/json',
Authorization: `Bearer ${runtime.token}`,
};
const serializedBody = body === undefined ? undefined : (() => {
try {
const encoded = JSON.stringify(body);
return encoded === undefined ? null : encoded;
} catch {
return null;
}
})();
if (serializedBody === null) throw invalidRequest();
if (serializedBody !== undefined) headers['Content-Type'] = 'application/json';
if (ifRevision !== undefined) headers['If-Match'] = `"${ifRevision}"`;
let response: FetchResponse;
try {
response = await fetchImpl(url, {
method,
headers,
...(serializedBody === undefined ? {} : { 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 undefined as T;
return parse(await parseJson(response));
}
function listPath(endpoint: string, collection: string, options: DataServiceListOptions): string {
const params = new URLSearchParams();
if (options.limit !== undefined) params.set('limit', String(options.limit));
if (options.cursor !== undefined) params.set('cursor', options.cursor);
const query = params.toString();
return `${endpoint}/collections/${encodeURIComponent(collection)}/documents${query ? `?${query}` : ''}`;
}
async function getDocument(collection: string, documentId: string): Promise<DataServiceDocument> {
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: string,
input: DataServiceListOptions = {},
): Promise<DataServiceDocumentPage> {
const runtime = readRuntime();
const safeCollection = requireCollection(collection);
const options = requireOptions(input, ['limit', 'cursor']);
const limit = options.limit;
if (limit !== undefined && (!positiveInteger(limit) || limit > 100)) throw invalidRequest();
const cursor = options.cursor;
if (cursor !== undefined && (!boundedText(cursor, MAX_CURSOR_LENGTH))) throw invalidRequest();
return await request(
'GET',
listPath(runtime.endpoint, safeCollection, {
...(limit === undefined ? {} : { limit }),
...(cursor === undefined ? {} : { cursor }),
}),
200,
parsePage,
);
}
async function putDocument(
collection: string,
documentId: string,
value: Record<string, unknown>,
input: DataServiceWriteOptions = {},
): Promise<DataServiceDocument> {
const runtime = readRuntime();
const safeCollection = requireCollection(collection);
const safeDocumentId = requireDocumentId(documentId);
const data = requireData(value);
const options = requireOptions(input, ['ifRevision']);
const ifRevision = readIfRevision(options.ifRevision);
return await request(
'PUT',
buildDocumentPath(runtime.endpoint, safeCollection, safeDocumentId),
200,
parseDocument,
{ data },
ifRevision,
);
}
async function deleteDocument(
collection: string,
documentId: string,
input: DataServiceWriteOptions = {},
): Promise<void> {
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,
() => undefined,
undefined,
ifRevision,
);
}
async function addDocument(
collection: string,
value: Record<string, unknown>,
): Promise<DataServiceDocument> {
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);
}
export const data = Object.freeze({
get: getDocument,
list: listDocuments,
put: putDocument,
delete: deleteDocument,
add: addDocument,
});
export default data;