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:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user