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,90 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export function buildPageDataPolicyScopeHash(policy) {
|
||||
const payload = {
|
||||
accessMode: policy?.accessMode ?? null,
|
||||
datasets: Object.keys(policy?.datasets ?? {}).sort(),
|
||||
};
|
||||
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
export async function upsertPageDataPolicyIndex(pool, policy) {
|
||||
if (!pool || !policy?.pageId || !policy?.ownerUserId) return null;
|
||||
const datasetCount = Object.keys(policy.datasets ?? {}).length;
|
||||
const now = Date.now();
|
||||
await pool.query(
|
||||
`INSERT INTO h5_page_data_policy_index
|
||||
(page_id, owner_user_id, access_mode, dataset_count, scope_hash, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
owner_user_id = VALUES(owner_user_id),
|
||||
access_mode = VALUES(access_mode),
|
||||
dataset_count = VALUES(dataset_count),
|
||||
scope_hash = VALUES(scope_hash),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
[
|
||||
policy.pageId,
|
||||
policy.ownerUserId,
|
||||
policy.accessMode,
|
||||
datasetCount,
|
||||
buildPageDataPolicyScopeHash(policy),
|
||||
now,
|
||||
],
|
||||
);
|
||||
return {
|
||||
pageId: policy.pageId,
|
||||
ownerUserId: policy.ownerUserId,
|
||||
accessMode: policy.accessMode,
|
||||
datasetCount,
|
||||
scopeHash: buildPageDataPolicyScopeHash(policy),
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPageDataPolicyIndex(pool, pageId) {
|
||||
if (!pool) return null;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT page_id, owner_user_id, access_mode, dataset_count, scope_hash, updated_at
|
||||
FROM h5_page_data_policy_index
|
||||
WHERE page_id = ?
|
||||
LIMIT 1`,
|
||||
[pageId],
|
||||
);
|
||||
const row = rows[0];
|
||||
if (!row) return null;
|
||||
return {
|
||||
pageId: row.page_id,
|
||||
ownerUserId: row.owner_user_id,
|
||||
accessMode: row.access_mode,
|
||||
datasetCount: Number(row.dataset_count ?? 0),
|
||||
scopeHash: row.scope_hash,
|
||||
updatedAt: Number(row.updated_at ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listPageDataPolicyIndexByOwner(pool, ownerUserId, { limit = 50 } = {}) {
|
||||
if (!pool) return [];
|
||||
const safeLimit = Math.min(Math.max(1, Number(limit ?? 50)), 200);
|
||||
const [rows] = await pool.query(
|
||||
`SELECT page_id, owner_user_id, access_mode, dataset_count, scope_hash, updated_at
|
||||
FROM h5_page_data_policy_index
|
||||
WHERE owner_user_id = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?`,
|
||||
[ownerUserId, safeLimit],
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
pageId: row.page_id,
|
||||
ownerUserId: row.owner_user_id,
|
||||
accessMode: row.access_mode,
|
||||
datasetCount: Number(row.dataset_count ?? 0),
|
||||
scopeHash: row.scope_hash,
|
||||
updatedAt: Number(row.updated_at ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function deletePageDataPolicyIndex(pool, pageId) {
|
||||
if (!pool || !pageId) return false;
|
||||
const [result] = await pool.query(`DELETE FROM h5_page_data_policy_index WHERE page_id = ?`, [pageId]);
|
||||
return Number(result?.affectedRows ?? 0) > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user