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
+87
View File
@@ -137,3 +137,90 @@ test('user-data-space-service supports update and soft delete', async () => {
const listed = service.readDatasetRows('tasks', { limit: 10 });
assert.equal(listed.rows.length, 0);
});
test('exportDatasetRows supports json and csv output', async () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT
);`);
await service.upsertDataset({
name: 'contacts',
table: 'contacts',
actions: ['read', 'insert'],
columns: {
read: ['id', 'name', 'phone'],
insert: ['name', 'phone'],
},
});
await service.insertDatasetRow('contacts', { name: '李四', phone: '13900000002' });
const json = service.exportDatasetRows('contacts', { format: 'json' });
assert.equal(json.rowCount, 1);
assert.equal(json.rows[0].name, '李四');
const csv = service.exportDatasetRows('contacts', { format: 'csv' });
assert.match(csv.content, /name,phone/);
assert.match(csv.content, /李四/);
});
test('restoreSoftDeletedRow clears deleted_at', async () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
deleted_at TEXT,
deleted_by TEXT
);`);
await service.upsertDataset({
name: 'items',
table: 'items',
actions: ['read', 'insert', 'soft_delete'],
columns: {
read: ['id', 'title', 'deleted_at'],
insert: ['title'],
soft_delete: ['id'],
},
});
const inserted = await service.insertDatasetRow('items', { title: '可恢复项' });
await service.softDeleteRowForDataset(service.getDataset('items'), inserted.row.id, { deletedBy: 'test' });
const hidden = service.readDatasetRows('items', { limit: 10 });
assert.equal(hidden.rows.length, 0);
const restored = await service.restoreSoftDeletedRow('items', inserted.row.id);
assert.equal(restored.restored, true);
const visible = service.readDatasetRows('items', { limit: 10 });
assert.equal(visible.rows.length, 1);
assert.equal(visible.rows[0].title, '可恢复项');
});
test('readRowsForDataset applies rowScope where clause', async () => {
const workspaceRoot = createTempWorkspace();
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE scoped_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
created_by_user_id TEXT
);`);
await service.upsertDataset({
name: 'scoped_items',
table: 'scoped_items',
actions: ['read', 'insert'],
columns: {
read: ['id', 'title', 'created_by_user_id'],
insert: ['title'],
},
});
await service.insertRowForDataset(service.getDataset('scoped_items'), { title: 'A' }, {
rowScope: { ownerColumn: 'created_by_user_id', visitorUserId: 'user-a' },
});
await service.insertRowForDataset(service.getDataset('scoped_items'), { title: 'B' }, {
rowScope: { ownerColumn: 'created_by_user_id', visitorUserId: 'user-b' },
});
const scoped = service.readRowsForDataset(service.getDataset('scoped_items'), {
limit: 10,
rowScope: { whereClause: `"created_by_user_id" = 'user-a'` },
});
assert.equal(scoped.rows.length, 1);
assert.equal(scoped.rows[0].title, 'A');
});