383 lines
16 KiB
JavaScript
383 lines
16 KiB
JavaScript
|
|
import fs from 'node:fs';
|
||
|
|
|
||
|
|
function randomInt(n) {
|
||
|
|
return Math.floor(Math.random() * n);
|
||
|
|
}
|
||
|
|
|
||
|
|
function generateBusinessGraph(numUsers = 5000, numGroups = 200, numDocs = 2000) {
|
||
|
|
const nodes = [];
|
||
|
|
const relations = [];
|
||
|
|
const adminKeys = [];
|
||
|
|
const newEmployeeKeys = [];
|
||
|
|
const testUserKeys = [];
|
||
|
|
|
||
|
|
console.log('🏗️ Generating realistic business authorization graph...');
|
||
|
|
console.log(`📊 Scale: ${numUsers} users, ${numGroups} groups, ${numDocs} documents`);
|
||
|
|
|
||
|
|
// Add organizational groups with realistic structure
|
||
|
|
const departments = ['engineering', 'product', 'design', 'marketing', 'sales', 'hr', 'finance', 'legal', 'operations', 'security', 'data', 'research'];
|
||
|
|
const levels = ['intern', 'junior', 'mid', 'senior', 'staff', 'principal', 'lead', 'manager', 'director', 'vp'];
|
||
|
|
const clearanceLevels = ['public', 'internal', 'confidential', 'restricted', 'secret', 'top_secret'];
|
||
|
|
const documentTypes = ['manual', 'specification', 'budget', 'contract', 'policy', 'report', 'plan'];
|
||
|
|
const currencies = ['usd', 'eur', 'gbp'];
|
||
|
|
|
||
|
|
console.log('📁 Creating organizational structure...');
|
||
|
|
|
||
|
|
// Create department groups
|
||
|
|
for (let i = 0; i < departments.length; i++) {
|
||
|
|
const dept = departments[i];
|
||
|
|
nodes.push({ key: `group:${dept}`, type: 'group' });
|
||
|
|
|
||
|
|
// Create level-based subgroups within each department
|
||
|
|
for (let j = 0; j < levels.length; j++) {
|
||
|
|
const level = levels[j];
|
||
|
|
const groupKey = `group:${dept}_${level}`;
|
||
|
|
nodes.push({ key: groupKey, type: 'group' });
|
||
|
|
|
||
|
|
// Create hierarchy: department contains level groups
|
||
|
|
relations.push({ src: `group:${dept}`, rel: 'contains', dst: groupKey });
|
||
|
|
|
||
|
|
// Create management hierarchy (for computed_userset rules)
|
||
|
|
if (j < levels.length - 1) {
|
||
|
|
const higherLevel = levels[j + 1];
|
||
|
|
const higherGroupKey = `group:${dept}_${higherLevel}`;
|
||
|
|
relations.push({ src: higherGroupKey, rel: 'manages', dst: groupKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add projects and teams
|
||
|
|
const projectTypes = ['project', 'team', 'committee', 'guild', 'workgroup'];
|
||
|
|
for (let i = 0; i < numGroups - (departments.length * (levels.length + 1)); i++) {
|
||
|
|
const type = projectTypes[i % projectTypes.length];
|
||
|
|
nodes.push({ key: `group:${type}_${i}`, type: 'group' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add clearance levels, document types, and currencies
|
||
|
|
for (const clearance of clearanceLevels) {
|
||
|
|
nodes.push({ key: `clearance:${clearance}`, type: 'clearance' });
|
||
|
|
}
|
||
|
|
for (const docType of documentTypes) {
|
||
|
|
nodes.push({ key: `doctype:${docType}`, type: 'document_type' });
|
||
|
|
}
|
||
|
|
for (const currency of currencies) {
|
||
|
|
nodes.push({ key: `currency:${currency}`, type: 'currency' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add budget and cost tracking nodes for financial rules
|
||
|
|
for (let i = 0; i < departments.length; i++) {
|
||
|
|
const dept = departments[i];
|
||
|
|
nodes.push({ key: `budget:${dept}_2024`, type: 'budget' });
|
||
|
|
nodes.push({ key: `account:${dept}_operational`, type: 'account' });
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('👥 Creating users with realistic attributes...');
|
||
|
|
|
||
|
|
// Add users with realistic department/level assignments
|
||
|
|
for (let i = 0; i < numUsers; i++) {
|
||
|
|
const userKey = `user:${i}`;
|
||
|
|
nodes.push({ key: userKey, type: 'user' });
|
||
|
|
|
||
|
|
// Assign to department and level with realistic distribution
|
||
|
|
const dept = departments[randomInt(departments.length)];
|
||
|
|
const level = levels[Math.min(levels.length - 1, Math.floor(Math.abs(gaussianRandom()) * 3) + 2)]; // Bias toward mid-level
|
||
|
|
const deptGroupKey = `group:${dept}`;
|
||
|
|
const levelGroupKey = `group:${dept}_${level}`;
|
||
|
|
|
||
|
|
relations.push({ src: userKey, rel: 'member_of', dst: deptGroupKey });
|
||
|
|
relations.push({ src: userKey, rel: 'member_of', dst: levelGroupKey });
|
||
|
|
|
||
|
|
// Add user accounts with balances (for relational_comparator rules)
|
||
|
|
const accountKey = `account:user_${i}`;
|
||
|
|
nodes.push({ key: accountKey, type: 'account' });
|
||
|
|
relations.push({ src: userKey, rel: 'owns_account', dst: accountKey });
|
||
|
|
|
||
|
|
// Assign random balance (10K - 100K USD)
|
||
|
|
const balance = 10000 + randomInt(90000);
|
||
|
|
relations.push({ src: accountKey, rel: 'has_balance', dst: 'currency:usd', value: balance });
|
||
|
|
|
||
|
|
// Assign clearance level (higher levels get higher clearance)
|
||
|
|
const clearanceIndex = Math.min(
|
||
|
|
clearanceLevels.length - 1,
|
||
|
|
levels.indexOf(level) + randomInt(3)
|
||
|
|
);
|
||
|
|
const clearance = clearanceLevels[clearanceIndex];
|
||
|
|
relations.push({ src: userKey, rel: 'has_clearance', dst: `clearance:${clearance}` });
|
||
|
|
|
||
|
|
// Users join multiple cross-functional groups (realistic for large orgs)
|
||
|
|
const numGroups = Math.floor(Math.abs(gaussianRandom()) * 3) + 1; // 1-4 groups
|
||
|
|
for (let g = 0; g < numGroups; g++) {
|
||
|
|
if (Math.random() < 0.4) {
|
||
|
|
const projectGroup = `group:project_${randomInt(numGroups - (departments.length * (levels.length + 1)))}`;
|
||
|
|
relations.push({ src: userKey, rel: 'member_of', dst: projectGroup });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (i % 500 === 0) console.log(` 👤 Added ${i} users...`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add system admins with full access
|
||
|
|
console.log('🔐 Creating system administrators...');
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
const adminKey = `admin:${i}`;
|
||
|
|
nodes.push({ key: adminKey, type: 'admin' });
|
||
|
|
adminKeys.push(adminKey);
|
||
|
|
|
||
|
|
// Admins have top secret clearance and access to all groups
|
||
|
|
relations.push({ src: adminKey, rel: 'has_clearance', dst: 'clearance:top_secret' });
|
||
|
|
for (const dept of departments) {
|
||
|
|
relations.push({ src: adminKey, rel: 'superadmin', dst: `group:${dept}` });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Admins have large budgets
|
||
|
|
const adminAccount = `account:admin_${i}`;
|
||
|
|
nodes.push({ key: adminAccount, type: 'account' });
|
||
|
|
relations.push({ src: adminKey, rel: 'owns_account', dst: adminAccount });
|
||
|
|
relations.push({ src: adminAccount, rel: 'has_balance', dst: 'currency:usd', value: 1000000 });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add new employees (for inference testing - no direct access)
|
||
|
|
console.log('🆕 Creating new employees for inference testing...');
|
||
|
|
for (let i = 0; i < 50; i++) {
|
||
|
|
const newEmpKey = `newbie:${i}`;
|
||
|
|
nodes.push({ key: newEmpKey, type: 'user' });
|
||
|
|
newEmployeeKeys.push(newEmpKey);
|
||
|
|
|
||
|
|
// Give them basic attributes but limited document access
|
||
|
|
const dept = departments[randomInt(departments.length)];
|
||
|
|
const level = 'intern'; // New employees start as interns
|
||
|
|
relations.push({ src: newEmpKey, rel: 'member_of', dst: `group:${dept}` });
|
||
|
|
relations.push({ src: newEmpKey, rel: 'member_of', dst: `group:${dept}_${level}` });
|
||
|
|
relations.push({ src: newEmpKey, rel: 'has_clearance', dst: 'clearance:internal' });
|
||
|
|
|
||
|
|
// Give them small budgets
|
||
|
|
const newEmpAccount = `account:newbie_${i}`;
|
||
|
|
nodes.push({ key: newEmpAccount, type: 'account' });
|
||
|
|
relations.push({ src: newEmpKey, rel: 'owns_account', dst: newEmpAccount });
|
||
|
|
relations.push({ src: newEmpAccount, rel: 'has_balance', dst: 'currency:usd', value: 5000 + randomInt(10000) });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add test users with specific patterns for inference validation
|
||
|
|
console.log('🧪 Creating test users for inference validation...');
|
||
|
|
for (let i = 0; i < 100; i++) {
|
||
|
|
const testUserKey = `test_user:${i}`;
|
||
|
|
nodes.push({ key: testUserKey, type: 'user' });
|
||
|
|
testUserKeys.push(testUserKey);
|
||
|
|
|
||
|
|
// Create similar patterns to existing users
|
||
|
|
const dept = departments[i % departments.length];
|
||
|
|
const level = levels[Math.floor(i / departments.length) % levels.length];
|
||
|
|
relations.push({ src: testUserKey, rel: 'member_of', dst: `group:${dept}` });
|
||
|
|
relations.push({ src: testUserKey, rel: 'member_of', dst: `group:${dept}_${level}` });
|
||
|
|
|
||
|
|
const clearanceIndex = Math.min(clearanceLevels.length - 1, levels.indexOf(level) + 1);
|
||
|
|
relations.push({ src: testUserKey, rel: 'has_clearance', dst: `clearance:${clearanceLevels[clearanceIndex]}` });
|
||
|
|
|
||
|
|
// Give them moderate budgets
|
||
|
|
const testAccount = `account:test_${i}`;
|
||
|
|
nodes.push({ key: testAccount, type: 'account' });
|
||
|
|
relations.push({ src: testUserKey, rel: 'owns_account', dst: testAccount });
|
||
|
|
relations.push({ src: testAccount, rel: 'has_balance', dst: 'currency:usd', value: 15000 + randomInt(50000) });
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('📄 Creating documents with complex access patterns...');
|
||
|
|
|
||
|
|
// Create documents with rich metadata
|
||
|
|
const docKeys = [];
|
||
|
|
for (let i = 0; i < numDocs; i++) {
|
||
|
|
const docKey = `doc:${i}`;
|
||
|
|
nodes.push({ key: docKey, type: 'doc' });
|
||
|
|
docKeys.push(docKey);
|
||
|
|
|
||
|
|
// Classify document with bias toward lower classifications
|
||
|
|
const classificationIndex = Math.min(
|
||
|
|
clearanceLevels.length - 1,
|
||
|
|
Math.floor(Math.abs(gaussianRandom()) * 2) + 1
|
||
|
|
);
|
||
|
|
const classification = clearanceLevels[classificationIndex];
|
||
|
|
relations.push({ src: docKey, rel: 'classified_as', dst: `clearance:${classification}` });
|
||
|
|
|
||
|
|
// Assign document type
|
||
|
|
const docType = documentTypes[randomInt(documentTypes.length)];
|
||
|
|
relations.push({ src: docKey, rel: 'has_type', dst: `doctype:${docType}` });
|
||
|
|
|
||
|
|
// Assign document to departments
|
||
|
|
const owningDept = departments[randomInt(departments.length)];
|
||
|
|
relations.push({ src: docKey, rel: 'owned_by', dst: `group:${owningDept}` });
|
||
|
|
|
||
|
|
// Add cost information for budget documents (for relational_comparator rules)
|
||
|
|
if (docType === 'budget' || docType === 'contract') {
|
||
|
|
const cost = 1000 + randomInt(50000); // $1K - $50K
|
||
|
|
relations.push({ src: docKey, rel: 'has_cost', dst: 'currency:usd', value: cost });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate realistic access patterns
|
||
|
|
console.log('🔥 Generating complex authorization patterns...');
|
||
|
|
|
||
|
|
// Sort documents by "popularity" with more realistic distribution
|
||
|
|
const superPopularDocs = docKeys.slice(0, Math.floor(numDocs * 0.05)); // Top 5% - super popular
|
||
|
|
const popularDocs = docKeys.slice(Math.floor(numDocs * 0.05), Math.floor(numDocs * 0.25)); // Next 20% - popular
|
||
|
|
const commonDocs = docKeys.slice(Math.floor(numDocs * 0.25), Math.floor(numDocs * 0.70)); // Next 45% - common
|
||
|
|
const rareDocs = docKeys.slice(Math.floor(numDocs * 0.70)); // Bottom 30% - rare
|
||
|
|
|
||
|
|
console.log(` 📊 Super popular: ${superPopularDocs.length}, Popular: ${popularDocs.length}, Common: ${commonDocs.length}, Rare: ${rareDocs.length}`);
|
||
|
|
|
||
|
|
// Get all regular users (not admins/newbies/test users)
|
||
|
|
const regularUsers = Array.from({ length: numUsers }, (_, i) => `user:${i}`);
|
||
|
|
|
||
|
|
// Create GROUP-BASED access patterns (for tuple_to_userset rules)
|
||
|
|
console.log('👥 Creating group-based access patterns...');
|
||
|
|
for (const dept of departments) {
|
||
|
|
const deptGroupKey = `group:${dept}`;
|
||
|
|
|
||
|
|
// Department groups get access to documents they own
|
||
|
|
const deptDocs = docKeys.filter((_, i) => i % departments.length === departments.indexOf(dept));
|
||
|
|
for (const docKey of deptDocs.slice(0, 20)) { // Limit for performance
|
||
|
|
relations.push({ src: deptGroupKey, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// SUPER POPULAR DOCS: Mix of direct and group access
|
||
|
|
for (const docKey of superPopularDocs) {
|
||
|
|
// 50% direct access, 50% group access
|
||
|
|
if (Math.random() < 0.5) {
|
||
|
|
// Direct user access
|
||
|
|
const numUsersWithAccess = Math.floor(regularUsers.length * 0.4);
|
||
|
|
const usersWithAccess = new Set();
|
||
|
|
|
||
|
|
for (let i = 0; i < numUsersWithAccess; i++) {
|
||
|
|
const userIndex = Math.floor(Math.random() * regularUsers.length);
|
||
|
|
const userKey = regularUsers[userIndex];
|
||
|
|
if (!usersWithAccess.has(userKey)) {
|
||
|
|
usersWithAccess.add(userKey);
|
||
|
|
relations.push({ src: userKey, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Group-based access (will require tuple_to_userset rule)
|
||
|
|
const owningDept = departments[randomInt(departments.length)];
|
||
|
|
relations.push({ src: `group:${owningDept}`, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// POPULAR DOCS: Mostly group access
|
||
|
|
for (const docKey of popularDocs) {
|
||
|
|
if (Math.random() < 0.8) {
|
||
|
|
// Group access
|
||
|
|
const owningDept = departments[randomInt(departments.length)];
|
||
|
|
relations.push({ src: `group:${owningDept}`, rel: 'can_read', dst: docKey });
|
||
|
|
} else {
|
||
|
|
// Limited direct access
|
||
|
|
const numUsersWithAccess = Math.floor(regularUsers.length * 0.1);
|
||
|
|
const usersWithAccess = new Set();
|
||
|
|
|
||
|
|
for (let i = 0; i < numUsersWithAccess; i++) {
|
||
|
|
const userIndex = Math.floor(Math.random() * regularUsers.length);
|
||
|
|
const userKey = regularUsers[userIndex];
|
||
|
|
if (!usersWithAccess.has(userKey)) {
|
||
|
|
usersWithAccess.add(userKey);
|
||
|
|
relations.push({ src: userKey, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// COMMON and RARE DOCS: Mostly group access
|
||
|
|
for (const docKey of [...commonDocs, ...rareDocs]) {
|
||
|
|
if (Math.random() < 0.9) {
|
||
|
|
// Group access
|
||
|
|
const owningDept = departments[randomInt(departments.length)];
|
||
|
|
relations.push({ src: `group:${owningDept}`, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Give admins direct access to all documents
|
||
|
|
console.log('🔐 Granting admin access...');
|
||
|
|
for (const adminKey of adminKeys) {
|
||
|
|
for (const docKey of docKeys) {
|
||
|
|
relations.push({ src: adminKey, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create patterns for new employees and test users
|
||
|
|
console.log('🎯 Creating inference-friendly patterns...');
|
||
|
|
|
||
|
|
// Give new employees limited direct access
|
||
|
|
for (const newEmpKey of newEmployeeKeys) {
|
||
|
|
const docsToAccess = Math.floor(superPopularDocs.length * 0.1);
|
||
|
|
const accessedDocs = new Set();
|
||
|
|
for (let i = 0; i < docsToAccess; i++) {
|
||
|
|
const docKey = superPopularDocs[Math.floor(Math.random() * superPopularDocs.length)];
|
||
|
|
if (!accessedDocs.has(docKey)) {
|
||
|
|
accessedDocs.add(docKey);
|
||
|
|
relations.push({ src: newEmpKey, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Give test users mixed access patterns
|
||
|
|
for (const testUserKey of testUserKeys) {
|
||
|
|
const superPopularAccess = Math.floor(superPopularDocs.length * 0.2);
|
||
|
|
const accessedDocs = new Set();
|
||
|
|
|
||
|
|
for (let i = 0; i < superPopularAccess; i++) {
|
||
|
|
const docKey = superPopularDocs[Math.floor(Math.random() * superPopularDocs.length)];
|
||
|
|
if (!accessedDocs.has(docKey)) {
|
||
|
|
accessedDocs.add(docKey);
|
||
|
|
relations.push({ src: testUserKey, rel: 'can_read', dst: docKey });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('✅ Complex graph generation complete!');
|
||
|
|
console.log(`📊 Final stats:`);
|
||
|
|
console.log(` • ${nodes.length} nodes`);
|
||
|
|
console.log(` • ${relations.length} relations`);
|
||
|
|
console.log(` • ${adminKeys.length} admins`);
|
||
|
|
console.log(` • ${newEmployeeKeys.length} new employees`);
|
||
|
|
console.log(` • ${testUserKeys.length} test users`);
|
||
|
|
console.log(` • ${departments.length} departments with ${levels.length} levels each`);
|
||
|
|
console.log(` • ${clearanceLevels.length} clearance levels`);
|
||
|
|
console.log(` • ${documentTypes.length} document types`);
|
||
|
|
|
||
|
|
return {
|
||
|
|
nodes,
|
||
|
|
relations,
|
||
|
|
adminKeys,
|
||
|
|
newEmployeeKeys,
|
||
|
|
testUserKeys,
|
||
|
|
departments,
|
||
|
|
levels,
|
||
|
|
clearanceLevels,
|
||
|
|
documentTypes,
|
||
|
|
currencies
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Gaussian random number generator for more realistic distributions
|
||
|
|
function gaussianRandom() {
|
||
|
|
let u = 0, v = 0;
|
||
|
|
while(u === 0) u = Math.random(); // Converting [0,1) to (0,1)
|
||
|
|
while(v === 0) v = Math.random();
|
||
|
|
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate different sized graphs for testing - MUCH BIGGER
|
||
|
|
const configs = [
|
||
|
|
{ name: 'small', users: 1000, groups: 100, docs: 500 },
|
||
|
|
{ name: 'medium', users: 5000, groups: 250, docs: 2000 },
|
||
|
|
{ name: 'large', users: 10000, groups: 500, docs: 5000 },
|
||
|
|
{ name: 'huge', users: 25000, groups: 1000, docs: 10000 }
|
||
|
|
];
|
||
|
|
|
||
|
|
const configName = process.argv[2] || 'medium';
|
||
|
|
const config = configs.find(c => c.name === configName) || configs[1];
|
||
|
|
|
||
|
|
console.log(`🎯 Generating ${config.name} graph configuration...`);
|
||
|
|
const data = generateBusinessGraph(config.users, config.groups, config.docs);
|
||
|
|
|
||
|
|
const filename = `big-graph-data-${config.name}.json`;
|
||
|
|
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
|
||
|
|
console.log(`💾 Saved to ${filename}`);
|
||
|
|
console.log(`🚀 Run: node big-graph.test.js ${config.name}`);
|