168 lines
4.1 KiB
JavaScript
168 lines
4.1 KiB
JavaScript
|
|
/**
|
||
|
|
* Predicate Resolver
|
||
|
|
*
|
||
|
|
* Maps predicate names to existing compiled DSL rules across all graph scopes.
|
||
|
|
* Does NOT create new rules - only looks up existing ones.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export class PredicateResolver {
|
||
|
|
constructor(context) {
|
||
|
|
this.context = context;
|
||
|
|
this.graphStores = context.graphStores || {};
|
||
|
|
this.cache = new Map();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resolve a predicate name to its DSL rule
|
||
|
|
*
|
||
|
|
* @param {string} predicateName - Name of the predicate
|
||
|
|
* @returns {Object|null} Rule info or null if not found
|
||
|
|
*/
|
||
|
|
resolve(predicateName) {
|
||
|
|
// Check cache first
|
||
|
|
if (this.cache.has(predicateName)) {
|
||
|
|
return this.cache.get(predicateName);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Look up in all graph scopes
|
||
|
|
const rule = this.findRule(predicateName);
|
||
|
|
|
||
|
|
if (rule) {
|
||
|
|
this.cache.set(predicateName, rule);
|
||
|
|
}
|
||
|
|
|
||
|
|
return rule;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Find a rule across all graph scopes
|
||
|
|
* Prefers logical rules over direct rules for evidence predicates
|
||
|
|
*/
|
||
|
|
findRule(predicateName) {
|
||
|
|
const scopes = [
|
||
|
|
'tenantExternal',
|
||
|
|
'tenantInternal',
|
||
|
|
'rootExternal',
|
||
|
|
'rootInternal',
|
||
|
|
'masterExternal',
|
||
|
|
'masterInternal'
|
||
|
|
];
|
||
|
|
|
||
|
|
let directRule = null;
|
||
|
|
let directScope = null;
|
||
|
|
|
||
|
|
for (const scopeName of scopes) {
|
||
|
|
const graphStore = this.graphStores[scopeName];
|
||
|
|
if (!graphStore) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
|
||
|
|
const arbiter = graphStore.arbiter || graphStore;
|
||
|
|
const config = arbiter.relationConfigs?.get(predicateName);
|
||
|
|
|
||
|
|
if (config) {
|
||
|
|
// Prefer logical rules (intersection/union) over direct rules
|
||
|
|
// This ensures evidence rules work correctly across all scopes
|
||
|
|
if (config.type === 'intersection' || config.type === 'union' || config.type === 'logical') {
|
||
|
|
return {
|
||
|
|
name: predicateName,
|
||
|
|
scope: scopeName,
|
||
|
|
config: config,
|
||
|
|
arity: this.inferArity(config)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remember the first direct rule as fallback
|
||
|
|
if (!directRule && config.type === 'direct') {
|
||
|
|
directRule = config;
|
||
|
|
directScope = scopeName;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return direct rule if no logical rule found
|
||
|
|
if (directRule) {
|
||
|
|
return {
|
||
|
|
name: predicateName,
|
||
|
|
scope: directScope,
|
||
|
|
config: directRule,
|
||
|
|
arity: this.inferArity(directRule)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Infer the arity (parameter count) from rule config
|
||
|
|
*/
|
||
|
|
inferArity(config) {
|
||
|
|
// Most DSL evidence rules have 1 or 2 parameters:
|
||
|
|
// - 1 param: just the subject (user)
|
||
|
|
// - 2 params: subject (user) + object
|
||
|
|
|
||
|
|
if (config.arity) {
|
||
|
|
return config.arity;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default to checking if it's a relation that typically needs an object
|
||
|
|
// This is a heuristic - in practice, the DSL defines this explicitly
|
||
|
|
if (config.type === 'tuple_to_userset' || config.type === 'direct') {
|
||
|
|
return 2; // Likely needs subject + object
|
||
|
|
}
|
||
|
|
|
||
|
|
return 1; // Default to 1 param
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if a predicate exists without full resolution
|
||
|
|
*/
|
||
|
|
exists(predicateName) {
|
||
|
|
return this.resolve(predicateName) !== null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all available predicates across all scopes
|
||
|
|
*/
|
||
|
|
getAllPredicates() {
|
||
|
|
const predicates = [];
|
||
|
|
const scopes = [
|
||
|
|
'tenantExternal',
|
||
|
|
'tenantInternal',
|
||
|
|
'rootExternal',
|
||
|
|
'rootInternal',
|
||
|
|
'masterExternal',
|
||
|
|
'masterInternal'
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const scopeName of scopes) {
|
||
|
|
const graphStore = this.graphStores[scopeName];
|
||
|
|
if (!graphStore) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle both: Arbiter directly (has .check()) or wrapper with .arbiter
|
||
|
|
const arbiter = graphStore.arbiter || graphStore;
|
||
|
|
if (arbiter.relationConfigs) {
|
||
|
|
for (const [name, config] of arbiter.relationConfigs) {
|
||
|
|
predicates.push({
|
||
|
|
name,
|
||
|
|
scope: scopeName,
|
||
|
|
arity: this.inferArity(config)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return predicates;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clear the cache (useful for testing or when rules change)
|
||
|
|
*/
|
||
|
|
clearCache() {
|
||
|
|
this.cache.clear();
|
||
|
|
}
|
||
|
|
}
|