feat(page-data): complete Phase 4-5, ops UI, and publish integration
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>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
function normalizeApiBase(apiBase) {
|
||||
var base = String(apiBase || '/api').trim() || '/api';
|
||||
return base.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function buildAuthPath(apiBase, pageId) {
|
||||
return normalizeApiBase(apiBase) + '/public/pages/' + encodeURIComponent(String(pageId || '').trim()) + '/data-auth';
|
||||
}
|
||||
|
||||
function buildDataPath(apiBase, pageId, dataset, suffix) {
|
||||
var base = normalizeApiBase(apiBase);
|
||||
var tail = suffix ? (String(suffix).charAt(0) === '/' ? suffix : '/' + suffix) : '';
|
||||
return base + '/public/pages/' + encodeURIComponent(String(pageId || '').trim()) + '/data/' + encodeURIComponent(String(dataset || '').trim()) + tail;
|
||||
}
|
||||
|
||||
function buildAuthHeaders(token) {
|
||||
var headers = { accept: 'application/json' };
|
||||
if (token) headers['x-page-data-token'] = token;
|
||||
return headers;
|
||||
}
|
||||
|
||||
function resolveCaptchaToken(options) {
|
||||
options = options || {};
|
||||
return options.captchaToken || options.turnstileToken || options.captcha_token || null;
|
||||
}
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
var payload = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok) {
|
||||
var error = (payload && payload.error) || {};
|
||||
var err = new Error(error.message || '页面数据请求失败');
|
||||
err.code = error.code || 'page_data_failed';
|
||||
err.status = response.status;
|
||||
throw err;
|
||||
}
|
||||
return (payload && payload.data) || payload;
|
||||
}
|
||||
|
||||
function createClient(options) {
|
||||
options = options || {};
|
||||
var pageId = options.pageId;
|
||||
var apiBase = options.apiBase || '/api';
|
||||
var fetchImpl = options.fetchImpl || global.fetch;
|
||||
var sessionToken = options.token || null;
|
||||
if (!pageId) throw new Error('pageId 不能为空');
|
||||
if (typeof fetchImpl !== 'function') throw new Error('fetch 不可用');
|
||||
|
||||
async function request(method, path, opts) {
|
||||
opts = opts || {};
|
||||
var url = new URL(path, global.location ? global.location.origin : 'http://localhost');
|
||||
if (opts.query) {
|
||||
Object.keys(opts.query).forEach(function (key) {
|
||||
var value = opts.query[key];
|
||||
if (value == null || value === '') return;
|
||||
url.searchParams.set(key, String(value));
|
||||
});
|
||||
}
|
||||
var headers = Object.assign(
|
||||
{},
|
||||
opts.body ? { 'content-type': 'application/json' } : {},
|
||||
opts.auth === false ? { accept: 'application/json' } : buildAuthHeaders(sessionToken),
|
||||
opts.extraHeaders || {},
|
||||
);
|
||||
var response = await fetchImpl(url.toString(), {
|
||||
method: method,
|
||||
headers: headers,
|
||||
credentials: 'include',
|
||||
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
||||
});
|
||||
return parseJsonResponse(response);
|
||||
}
|
||||
|
||||
return {
|
||||
get pageId() { return pageId; },
|
||||
get token() { return sessionToken; },
|
||||
setToken: function (nextToken) { sessionToken = nextToken ? String(nextToken) : null; },
|
||||
authenticate: function (password) {
|
||||
return request('POST', buildAuthPath(apiBase, pageId), { body: { password: password }, auth: false })
|
||||
.then(function (result) {
|
||||
if (result && result.token) sessionToken = result.token;
|
||||
return result;
|
||||
});
|
||||
},
|
||||
listRows: function (dataset, query) {
|
||||
return request('GET', buildDataPath(apiBase, pageId, dataset), { query: query || {} });
|
||||
},
|
||||
getSchema: function (dataset) {
|
||||
return request('GET', buildDataPath(apiBase, pageId, dataset, 'schema'));
|
||||
},
|
||||
getStats: function (dataset) {
|
||||
return request('GET', buildDataPath(apiBase, pageId, dataset, 'stats'));
|
||||
},
|
||||
insertRow: function (dataset, row, insertOptions) {
|
||||
var captchaToken = resolveCaptchaToken(insertOptions);
|
||||
return request('POST', buildDataPath(apiBase, pageId, dataset, 'rows'), {
|
||||
body: row,
|
||||
extraHeaders: captchaToken ? { 'x-page-data-captcha': String(captchaToken) } : {},
|
||||
});
|
||||
},
|
||||
updateRow: function (dataset, rowId, row) {
|
||||
return request('PATCH', buildDataPath(apiBase, pageId, dataset, 'rows/' + rowId), { body: row });
|
||||
},
|
||||
deleteRow: function (dataset, rowId) {
|
||||
return request('DELETE', buildDataPath(apiBase, pageId, dataset, 'rows/' + rowId));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
global.MindSpacePageData = {
|
||||
createClient: createClient,
|
||||
buildAuthPath: buildAuthPath,
|
||||
buildDataPath: buildDataPath,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
Reference in New Issue
Block a user