Files
my-pi/pi-permission-system/src/scope-merge.ts
T

73 lines
3.0 KiB
TypeScript

import { mergeFlatPermissions } from "#src/permission-merge";
import type { RuleOrigin } from "#src/rule";
import type { FlatPermissionConfig, ScopeConfig } from "#src/types";
/** Surface → (pattern → originating scope). */
type OriginMap = Map<string, Map<string, RuleOrigin>>;
/** Result of merging permission objects across scopes with provenance tracking. */
export interface MergedScopes {
/** Fully merged flat permission config (lowest → highest precedence). */
mergedPermission: FlatPermissionConfig;
/** Maps each surface to a per-pattern origin (which scope contributed it). */
origins: OriginMap;
}
/**
* Merge permission objects across scopes (lowest → highest precedence) while
* tracking which scope contributed each (surface, pattern) entry.
*
* Mirrors mergeFlatPermissions() semantics for origin attribution:
* - Both base and incoming are objects → shallow-merge: each incoming pattern
* is attributed to this scope; patterns the higher scope does not redefine
* keep their earlier origin.
* - Otherwise → full replacement: this scope takes over the entire surface
* entry, discarding all lower-scope attribution.
*/
export function mergeScopesWithOrigins(
scopes: readonly (readonly [RuleOrigin, ScopeConfig])[],
): MergedScopes {
const origins: OriginMap = new Map();
let mergedPermission: FlatPermissionConfig = {};
for (const [scopeName, scope] of scopes) {
if (!scope.permission) continue;
for (const [surface, value] of Object.entries(scope.permission)) {
const baseVal = mergedPermission[surface];
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- defensive null/type checks; config values may differ at runtime */
const bothObjects =
typeof baseVal === "object" &&
baseVal !== null &&
typeof value === "object" &&
value !== null;
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
if (bothObjects) {
// Shallow-merge: each incoming pattern is attributed to this scope;
// existing patterns from lower scopes keep their earlier origin.
if (!origins.has(surface)) origins.set(surface, new Map());
for (const pattern of Object.keys(value)) {
origins.get(surface)?.set(pattern, scopeName);
}
} else {
// Full replacement: this scope takes over the entire surface entry.
const surfaceOrigins = new Map<string, RuleOrigin>();
if (typeof value === "string") {
surfaceOrigins.set("*", scopeName);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive null check
} else if (typeof value === "object" && value !== null) {
for (const pattern of Object.keys(value)) {
surfaceOrigins.set(pattern, scopeName);
}
}
origins.set(surface, surfaceOrigins);
}
}
mergedPermission = mergeFlatPermissions(mergedPermission, scope.permission);
}
return { mergedPermission, origins };
}