Files
pgmorbac-fastify/src/org-paths.ts
T

38 lines
1.8 KiB
TypeScript

import type { Pool } from 'pg';
import { permittedOrgs } from './permissions.js';
/**
* Wraps a query selecting the visible orgs (id, name, parent_id, metadata) and
* computes each org's path as the names from the nearest visible ancestor down
* to it. Recursion is seeded from orgs whose parent is absent from the visible
* set, so a path can never name an ancestor the caller cannot read. Rows come
* back parent-first (ORDER BY path).
*/
export function withOrgPaths(visSql: string): string {
return `WITH RECURSIVE vis AS (${visSql}),
tree AS (
SELECT v.id, v.name, v.parent_id, v.metadata, ARRAY[v.name]::text[] AS path
FROM vis v
WHERE v.parent_id IS NULL OR NOT EXISTS (SELECT 1 FROM vis p WHERE p.id = v.parent_id)
UNION ALL
SELECT v.id, v.name, v.parent_id, v.metadata, t.path || v.name
FROM vis v JOIN tree t ON v.parent_id = t.id
)
SELECT id, name, parent_id, metadata, path FROM tree ORDER BY path`;
}
/**
* Ancestor-path names, keyed by org id, for every org the user may read under
* `view`. The truncation is the point: a caller who cannot read a parent sees
* the path start at the topmost org they can read, never above it.
*/
export async function readableOrgPaths(db: Pool, userId: string, view: string): Promise<Map<string, string[]>> {
const { rows: candidates } = await db.query<{ id: string }>(
`SELECT id FROM morbac.orgs WHERE morbac.is_allowed($1::UUID, id, 'read', $2)`, [userId, view]);
const readable = await permittedOrgs(db, userId, candidates.map((c) => c.id), 'read', view);
const { rows } = await db.query<{ id: string; path: string[] }>(
withOrgPaths('SELECT id, name, parent_id, NULL::jsonb AS metadata FROM morbac.orgs WHERE id = ANY($1::UUID[])'),
[readable]);
return new Map(rows.map((r) => [r.id, r.path]));
}