import { assertSafeSqlIdentifier, normalizeDatasetConfig } from './user-data-space-service.mjs'; import { normalizeRowPolicy, normalizeRoleOverrides, normalizeVisitorAssignments, } from './page-access-visitor.mjs'; export const PAGE_DATA_ACCESS_MODES = new Set(['public', 'password', 'login_required']); export function normalizePolicyDataset(name, raw) { const datasetName = assertSafeSqlIdentifier(name, 'dataset 名称'); if (!raw || typeof raw !== 'object') { throw Object.assign(new Error(`dataset 策略无效:${datasetName}`), { code: 'invalid_policy' }); } const columns = raw.columns && typeof raw.columns === 'object' ? raw.columns : {}; const normalizedColumns = {}; for (const [action, fields] of Object.entries(columns)) { if (!Array.isArray(fields)) continue; normalizedColumns[action] = fields.map((field) => assertSafeSqlIdentifier(field, `${action} 字段`)); } return { name: datasetName, read: Boolean(raw.read), insert: Boolean(raw.insert), update: Boolean(raw.update), softDelete: Boolean(raw.softDelete ?? raw.soft_delete), // `delete` is the page-facing spelling for an intentional permanent PG // delete; retain hardDelete for existing policies. hardDelete: Boolean(raw.hardDelete ?? raw.hard_delete ?? raw.delete), columns: normalizedColumns, limits: raw.limits && typeof raw.limits === 'object' ? { maxRowsPerRead: Number(raw.limits.maxRowsPerRead ?? 100), maxInsertBytes: Number(raw.limits.maxInsertBytes ?? 8192), } : undefined, rowPolicy: normalizeRowPolicy(raw.rowPolicy ?? raw.row_policy), closed: Boolean(raw.closed), closedAt: raw.closedAt ?? raw.closed_at ?? null, }; } export function normalizePageAccessPolicy(raw, { fallbackPageId = null, fallbackOwnerUserId = null } = {}) { if (!raw || typeof raw !== 'object') { throw Object.assign(new Error('Page Access Policy 无效'), { code: 'invalid_policy' }); } const pageId = String(raw.pageId ?? fallbackPageId ?? '').trim(); const ownerUserId = String(raw.ownerUserId ?? fallbackOwnerUserId ?? '').trim(); if (!pageId) throw Object.assign(new Error('缺少 pageId'), { code: 'invalid_policy' }); if (!ownerUserId) throw Object.assign(new Error('缺少 ownerUserId'), { code: 'invalid_policy' }); const accessMode = String(raw.accessMode ?? raw.access_mode ?? 'public').trim(); if (!PAGE_DATA_ACCESS_MODES.has(accessMode)) { throw Object.assign(new Error(`不支持的 accessMode:${accessMode}`), { code: 'invalid_policy' }); } const datasetsInput = raw.datasets && typeof raw.datasets === 'object' ? raw.datasets : {}; const datasets = {}; for (const [name, config] of Object.entries(datasetsInput)) { datasets[assertSafeSqlIdentifier(name, 'dataset 名称')] = normalizePolicyDataset(name, config); } const defaultVisitorRole = String( raw.defaultVisitorRole ?? raw.default_visitor_role ?? (accessMode === 'login_required' ? 'viewer' : 'deny'), ).trim(); if (!['deny', 'viewer', 'editor'].includes(defaultVisitorRole)) { throw Object.assign(new Error(`不支持的 defaultVisitorRole:${defaultVisitorRole}`), { code: 'invalid_policy', }); } return { pageId, ownerUserId, workspaceRef: String(raw.workspaceRef ?? raw.workspace_ref ?? '').trim() || null, accessMode, defaultVisitorRole, visitors: normalizeVisitorAssignments(raw.visitors), roles: normalizeRoleOverrides(raw.roles), datasets, updatedAt: raw.updatedAt ?? new Date().toISOString(), }; } export function buildEffectiveDataset(registryDataset, policyDataset) { if (!registryDataset || !policyDataset) return null; const actions = []; if (policyDataset.read) actions.push('read'); if (policyDataset.insert) actions.push('insert'); if (policyDataset.update) actions.push('update'); if (policyDataset.softDelete) actions.push('soft_delete'); if (policyDataset.hardDelete) actions.push('hard_delete'); const columns = { read: policyDataset.columns?.read ?? registryDataset.columns?.read ?? [], insert: policyDataset.columns?.insert ?? registryDataset.columns?.insert ?? [], update: policyDataset.columns?.update ?? registryDataset.columns?.update ?? [], soft_delete: policyDataset.columns?.soft_delete ?? registryDataset.columns?.soft_delete ?? ['id'], }; return normalizeDatasetConfig({ name: registryDataset.name, table: registryDataset.table, description: registryDataset.description, actions, columns, limits: policyDataset.limits ?? registryDataset.limits, }); } export function defaultDatasetsForAccessMode(accessMode, datasets = {}) { const normalized = {}; for (const [name, config] of Object.entries(datasets)) { const base = normalizePolicyDataset(name, config); if (accessMode === 'public') { normalized[name] = { ...base, read: false, update: false, softDelete: false, hardDelete: false, }; } else if (accessMode === 'password') { normalized[name] = { ...base, hardDelete: false, }; } else { normalized[name] = { ...base, hardDelete: false, }; } } return normalized; } export function policyAllowsAction(policy, datasetName, action) { const dataset = policy?.datasets?.[datasetName]; if (!dataset) return false; if (dataset.closed) return false; if (action === 'read') return dataset.read; if (action === 'insert') return dataset.insert; if (action === 'update') return dataset.update; if (action === 'soft_delete') return dataset.softDelete; if (action === 'hard_delete') return dataset.hardDelete; return false; } export function closePolicyDataset(policy, datasetName) { const dataset = policy?.datasets?.[datasetName]; if (!dataset) { throw Object.assign(new Error('dataset 未授权'), { code: 'dataset_not_found' }); } return { ...policy, datasets: { ...policy.datasets, [datasetName]: { ...dataset, read: false, insert: false, update: false, softDelete: false, hardDelete: false, closed: true, closedAt: new Date().toISOString(), }, }, updatedAt: new Date().toISOString(), }; } export function mapPublicationAccessModeToPageData(accessMode) { const mode = String(accessMode ?? '').trim(); if (PAGE_DATA_ACCESS_MODES.has(mode)) return mode; return null; } export function buildDatasetPolicyFromRegistry( registryDataset, { read = false, insert = false, update = false, softDelete = false, accessMode = 'public' } = {}, ) { const rowPolicy = accessMode === 'login_required' && (update || softDelete || read) ? { scope: 'own_rows', ownerColumn: 'created_by_user_id' } : null; return normalizePolicyDataset(registryDataset?.name ?? 'dataset', { read: Boolean(read), insert: Boolean(insert), update: Boolean(update), softDelete: Boolean(softDelete), columns: { read: registryDataset?.columns?.read ?? [], insert: registryDataset?.columns?.insert ?? [], update: registryDataset?.columns?.update ?? [], soft_delete: registryDataset?.columns?.soft_delete ?? ['id'], }, rowPolicy, }); } export function buildPageDataPolicyFromPublishInput({ pageId, ownerUserId, accessMode, datasetName, registryDataset, capabilities = {}, }) { const mappedAccessMode = mapPublicationAccessModeToPageData(accessMode); if (!mappedAccessMode) { throw Object.assign(new Error('当前发布访问模式不支持 Page Data'), { code: 'invalid_policy' }); } const defaults = mappedAccessMode === 'public' ? { read: false, insert: true, update: false, softDelete: false } : mappedAccessMode === 'password' ? { read: true, insert: true, update: false, softDelete: false } : { read: true, insert: true, update: true, softDelete: false }; const datasetPolicy = buildDatasetPolicyFromRegistry(registryDataset, { read: capabilities.read ?? defaults.read, insert: capabilities.insert ?? defaults.insert, update: capabilities.update ?? defaults.update, softDelete: capabilities.softDelete ?? defaults.softDelete, accessMode: mappedAccessMode, }); return normalizePageAccessPolicy( { pageId, ownerUserId, accessMode: mappedAccessMode, datasets: { [datasetName]: datasetPolicy, }, }, { fallbackPageId: pageId, fallbackOwnerUserId: ownerUserId }, ); }