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:
john
2026-07-08 14:52:49 +08:00
parent 8d01553469
commit 6b0c633a75
40 changed files with 4056 additions and 60 deletions
+95
View File
@@ -55,6 +55,33 @@ async function setupWorkspace(workspaceRoot) {
return service;
}
async function setupPublicWorkspace(workspaceRoot) {
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE signups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT,
status TEXT DEFAULT 'pending',
created_at TEXT,
deleted_at TEXT,
deleted_by TEXT,
updated_at TEXT,
updated_by_label TEXT
);`);
await service.upsertDataset({
name: 'signups',
table: 'signups',
actions: ['read', 'insert', 'update', 'soft_delete'],
columns: {
read: ['id', 'name', 'phone', 'status', 'created_at'],
insert: ['name', 'phone', 'status'],
update: ['status'],
soft_delete: ['id'],
},
});
return service;
}
function buildApp(workspaceRoot) {
const pageDataPublicService = createPageDataPublicService({
getPool: () => createPool('public'),
@@ -252,3 +279,71 @@ test('integration: password publication flow still works for read after data-aut
assert.equal(listed.status, 200);
assert.equal(listed.body.data.rows[0].title, '口令可见');
});
test('integration: public insert writes operation logs for owner review', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-logs-int-'));
await setupPublicWorkspace(workspaceRoot);
const app = buildApp(workspaceRoot);
await request(app, 'PUT', `/api/page-data/policies/${PAGE_ID}`, {
headers: { 'x-test-user': '1' },
body: {
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
accessMode: 'public',
datasets: { signups: { insert: true, columns: { insert: ['name', 'phone', 'status'] } } },
},
});
await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/signups/rows`, {
body: { name: '日志测试', phone: '13800000000' },
});
const logs = await request(app, 'GET', `/api/page-data/policies/${PAGE_ID}/logs`, {
headers: { 'x-test-user': '1' },
});
assert.equal(logs.status, 200);
assert.equal(logs.body.data.logs.length, 1);
assert.equal(logs.body.data.logs[0].action, 'insert');
assert.equal(logs.body.data.logs[0].dataset, 'signups');
});
test('integration: owner can export dataset and restore soft-deleted rows', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-owner-ops-'));
const ownerService = await setupPublicWorkspace(workspaceRoot);
const inserted = await ownerService.insertDatasetRow('signups', {
name: '导出项',
phone: '13800000001',
});
const app = buildApp(workspaceRoot);
const exported = await request(app, 'GET', '/api/page-data/signups/export?format=json', {
headers: { 'x-test-user': '1' },
});
assert.equal(exported.status, 200);
assert.equal(exported.body.data.rowCount, 1);
await ownerService.softDeleteRowForDataset(ownerService.getDataset('signups'), inserted.row.id, {
deletedBy: 'test',
});
const restored = await request(app, 'POST', `/api/page-data/signups/rows/${inserted.row.id}/restore`, {
headers: { 'x-test-user': '1' },
});
assert.equal(restored.status, 200);
assert.equal(restored.body.data.restored, true);
});
test('integration: apply-publish route binds dataset after publication', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-apply-publish-route-'));
await setupPublicWorkspace(workspaceRoot);
const app = buildApp(workspaceRoot);
const applied = await request(app, 'POST', `/api/page-data/policies/${PAGE_ID}/apply-publish`, {
headers: { 'x-test-user': '1' },
body: {
datasetName: 'signups',
capabilities: { read: true, insert: true, update: false, softDelete: false },
},
});
assert.equal(applied.status, 200);
assert.equal(applied.body.data.policy.datasets.signups.read, true);
const publicRead = await request(app, 'GET', `/api/public/pages/${PAGE_ID}/data/signups`);
assert.equal(publicRead.status, 403);
});