feat(coding): add generated Data Service SDK skill
This commit is contained in:
550
resources/coding-skills/data-service/assets/makelore-data.ts
Normal file
550
resources/coding-skills/data-service/assets/makelore-data.ts
Normal file
@@ -0,0 +1,550 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user