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:
+72
-9
@@ -1,4 +1,6 @@
|
||||
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
||||
import { listPageDataLogs } from './page-data-log-store.mjs';
|
||||
import { readPageAccessPolicy } from './page-data-policy-store.mjs';
|
||||
|
||||
function mapServiceError(error) {
|
||||
const code = error?.code;
|
||||
@@ -11,6 +13,9 @@ function mapServiceError(error) {
|
||||
if (code === 'columns_not_allowed') {
|
||||
return { status: 403, code: 'columns_not_allowed', message: error.message };
|
||||
}
|
||||
if (code === 'invalid_row_id' || code === 'restore_unsupported') {
|
||||
return { status: 400, code: code ?? 'invalid_request', message: error.message };
|
||||
}
|
||||
if (code === 'invalid_payload' || code === 'payload_too_large' || code === 'invalid_dataset_config') {
|
||||
return { status: 400, code: code ?? 'invalid_request', message: error.message };
|
||||
}
|
||||
@@ -40,21 +45,25 @@ export function createPageDataService(deps = {}) {
|
||||
throw Object.assign(new Error('用户工作区不存在'), { code: 'workspace_not_found' });
|
||||
}
|
||||
const pool = getPool();
|
||||
return createUserDataSpaceService({
|
||||
return {
|
||||
workspaceRoot,
|
||||
userId: user.id,
|
||||
query: pool ?? null,
|
||||
});
|
||||
service: createUserDataSpaceService({
|
||||
workspaceRoot,
|
||||
userId: user.id,
|
||||
query: pool ?? null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function listRows(user, datasetName, query = {}) {
|
||||
const service = await createServiceForUser(user);
|
||||
const { service } = await createServiceForUser(user);
|
||||
try {
|
||||
return service.readDatasetRows(datasetName, {
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
orderBy: query.orderBy ?? query.order_by,
|
||||
orderDir: query.orderDir ?? query.order_dir,
|
||||
includeDeleted: ['1', 'true', 'yes'].includes(String(query.include_deleted ?? '').toLowerCase()),
|
||||
});
|
||||
} catch (error) {
|
||||
throw mapServiceError(error);
|
||||
@@ -62,7 +71,7 @@ export function createPageDataService(deps = {}) {
|
||||
}
|
||||
|
||||
async function getSchema(user, datasetName) {
|
||||
const service = await createServiceForUser(user);
|
||||
const { service } = await createServiceForUser(user);
|
||||
try {
|
||||
return service.getDatasetSchema(datasetName);
|
||||
} catch (error) {
|
||||
@@ -71,7 +80,7 @@ export function createPageDataService(deps = {}) {
|
||||
}
|
||||
|
||||
async function getStats(user, datasetName) {
|
||||
const service = await createServiceForUser(user);
|
||||
const { service } = await createServiceForUser(user);
|
||||
try {
|
||||
return service.getDatasetStats(datasetName);
|
||||
} catch (error) {
|
||||
@@ -80,7 +89,7 @@ export function createPageDataService(deps = {}) {
|
||||
}
|
||||
|
||||
async function insertRow(user, datasetName, payload) {
|
||||
const service = await createServiceForUser(user);
|
||||
const { service } = await createServiceForUser(user);
|
||||
try {
|
||||
return service.insertDatasetRow(datasetName, payload);
|
||||
} catch (error) {
|
||||
@@ -88,8 +97,30 @@ export function createPageDataService(deps = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreRow(user, datasetName, rowId) {
|
||||
const { service } = await createServiceForUser(user);
|
||||
try {
|
||||
return service.restoreSoftDeletedRow(datasetName, rowId);
|
||||
} catch (error) {
|
||||
throw mapServiceError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportDataset(user, datasetName, query = {}) {
|
||||
const { service } = await createServiceForUser(user);
|
||||
try {
|
||||
return service.exportDatasetRows(datasetName, {
|
||||
format: query.format,
|
||||
includeDeleted: ['1', 'true', 'yes'].includes(String(query.include_deleted ?? '').toLowerCase()),
|
||||
limit: query.limit,
|
||||
});
|
||||
} catch (error) {
|
||||
throw mapServiceError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function listDatasets(user) {
|
||||
const service = await createServiceForUser(user);
|
||||
const { service } = await createServiceForUser(user);
|
||||
return service.listDatasets().map((dataset) => ({
|
||||
name: dataset.name,
|
||||
table: dataset.table,
|
||||
@@ -98,12 +129,44 @@ export function createPageDataService(deps = {}) {
|
||||
}));
|
||||
}
|
||||
|
||||
async function assertPageOwner(user, pageId) {
|
||||
const pool = getPool();
|
||||
if (pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT user_id FROM h5_publish_records
|
||||
WHERE page_id = ? AND status = 'online'
|
||||
ORDER BY published_at DESC
|
||||
LIMIT 1`,
|
||||
[pageId],
|
||||
);
|
||||
const ownerId = rows[0]?.user_id;
|
||||
if (ownerId && ownerId === user.id) return;
|
||||
}
|
||||
const workspaceRoot = await resolveWorkspaceRoot(user);
|
||||
const policy = readPageAccessPolicy(workspaceRoot, pageId);
|
||||
if (policy?.ownerUserId === user.id) return;
|
||||
throw Object.assign(new Error('无权访问该页面数据'), { code: 'forbidden', status: 403 });
|
||||
}
|
||||
|
||||
async function listPageLogs(user, pageId, query = {}) {
|
||||
await assertPageOwner(user, pageId);
|
||||
const { workspaceRoot } = await createServiceForUser(user);
|
||||
const logs = listPageDataLogs(workspaceRoot, pageId, {
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
});
|
||||
return { pageId, logs, count: logs.length };
|
||||
}
|
||||
|
||||
return {
|
||||
getUserAuth,
|
||||
listRows,
|
||||
getSchema,
|
||||
getStats,
|
||||
insertRow,
|
||||
restoreRow,
|
||||
exportDataset,
|
||||
listDatasets,
|
||||
listPageLogs,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user