6b0c633a75
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index, Turnstile captcha, browser client SDK, publish-panel dataset binding, acceptance tests, and usage documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
114 lines
4.0 KiB
JavaScript
114 lines
4.0 KiB
JavaScript
function normalizeApiBase(apiBase) {
|
|
const base = String(apiBase ?? '/api').trim() || '/api';
|
|
return base.replace(/\/$/, '');
|
|
}
|
|
|
|
export function buildPageDataAuthPath(apiBase, pageId) {
|
|
const base = normalizeApiBase(apiBase);
|
|
return `${base}/public/pages/${encodeURIComponent(String(pageId ?? '').trim())}/data-auth`;
|
|
}
|
|
|
|
export function buildPageDataPublicPath(apiBase, pageId, dataset, suffix = '') {
|
|
const base = normalizeApiBase(apiBase);
|
|
const safePageId = encodeURIComponent(String(pageId ?? '').trim());
|
|
const safeDataset = encodeURIComponent(String(dataset ?? '').trim());
|
|
const tail = suffix ? (suffix.startsWith('/') ? suffix : `/${suffix}`) : '';
|
|
return `${base}/public/pages/${safePageId}/data/${safeDataset}${tail}`;
|
|
}
|
|
|
|
function buildAuthHeaders(token) {
|
|
const headers = { accept: 'application/json' };
|
|
if (token) headers['x-page-data-token'] = token;
|
|
return headers;
|
|
}
|
|
|
|
async function parseJsonResponse(response) {
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const error = payload?.error ?? {};
|
|
throw Object.assign(new Error(error.message ?? '页面数据请求失败'), {
|
|
code: error.code ?? 'page_data_failed',
|
|
status: response.status,
|
|
});
|
|
}
|
|
return payload?.data ?? payload;
|
|
}
|
|
|
|
export function createPageDataBrowserClient({
|
|
pageId,
|
|
apiBase = '/api',
|
|
token = null,
|
|
fetchImpl = globalThis.fetch,
|
|
} = {}) {
|
|
if (!pageId) throw new Error('pageId 不能为空');
|
|
if (typeof fetchImpl !== 'function') throw new Error('fetch 不可用');
|
|
|
|
let sessionToken = token;
|
|
|
|
async function request(method, path, { body, query, auth = true, extraHeaders = {} } = {}) {
|
|
const url = new URL(path, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
|
|
if (query && typeof query === 'object') {
|
|
for (const [key, value] of Object.entries(query)) {
|
|
if (value == null || value === '') continue;
|
|
url.searchParams.set(key, String(value));
|
|
}
|
|
}
|
|
const headers = {
|
|
...(body ? { 'content-type': 'application/json' } : {}),
|
|
...(auth ? buildAuthHeaders(sessionToken) : { accept: 'application/json' }),
|
|
...(extraHeaders && typeof extraHeaders === 'object' ? extraHeaders : {}),
|
|
};
|
|
const response = await fetchImpl(url.toString(), {
|
|
method,
|
|
headers,
|
|
credentials: 'include',
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
return parseJsonResponse(response);
|
|
}
|
|
|
|
return {
|
|
get pageId() {
|
|
return pageId;
|
|
},
|
|
get token() {
|
|
return sessionToken;
|
|
},
|
|
setToken(nextToken) {
|
|
sessionToken = nextToken ? String(nextToken) : null;
|
|
},
|
|
async authenticate(password) {
|
|
const result = await request('POST', buildPageDataAuthPath(apiBase, pageId), {
|
|
body: { password },
|
|
auth: false,
|
|
});
|
|
if (result?.token) sessionToken = result.token;
|
|
return result;
|
|
},
|
|
async listRows(dataset, query = {}) {
|
|
return request('GET', buildPageDataPublicPath(apiBase, pageId, dataset), { query });
|
|
},
|
|
async getSchema(dataset) {
|
|
return request('GET', buildPageDataPublicPath(apiBase, pageId, dataset, 'schema'));
|
|
},
|
|
async getStats(dataset) {
|
|
return request('GET', buildPageDataPublicPath(apiBase, pageId, dataset, 'stats'));
|
|
},
|
|
async insertRow(dataset, row, options = {}) {
|
|
const captchaToken =
|
|
options.captchaToken ?? options.turnstileToken ?? options.captcha_token ?? null;
|
|
const extraHeaders = captchaToken ? { 'x-page-data-captcha': String(captchaToken) } : {};
|
|
return request('POST', buildPageDataPublicPath(apiBase, pageId, dataset, 'rows'), {
|
|
body: row,
|
|
extraHeaders,
|
|
});
|
|
},
|
|
async updateRow(dataset, rowId, row) {
|
|
return request('PATCH', buildPageDataPublicPath(apiBase, pageId, dataset, `rows/${rowId}`), { body: row });
|
|
},
|
|
async deleteRow(dataset, rowId) {
|
|
return request('DELETE', buildPageDataPublicPath(apiBase, pageId, dataset, `rows/${rowId}`));
|
|
},
|
|
};
|
|
}
|