UNPKG

@schoolai/spicedb-zed-schema-parser

Version:

SpiceDB .zed file format parser and analyzer written in Typescript

122 lines (105 loc) 4.11 kB
import { AugmentedObjectTypeDefinition, AugmentedSchemaAST, } from './semantic-analyzer/types' /** * Generates the TypeScript code for a permissions SDK based on a zed schema. * @param schema The augmented schema AST. * @returns The generated TypeScript code as a string. */ export function generateSDK( schema: AugmentedSchemaAST, parserImport = '@schoolai/spicedb-zed-schema-parser', ): string { const objectDefs = schema.definitions.filter( (def): def is AugmentedObjectTypeDefinition => def.type === 'definition', ) let code = `// Generated by @schoolai/spicedb-zed-schema-parser // Do not edit manually. import { PermissionOperations } from '${parserImport}'; // --------------- GENERIC TYPES --------------- export type Subject<T extends string> = \`\${T}:\${string}\`; export type Resource<T extends string> = \`\${T}:\${string}\`; // --------------- RESOURCE TYPES --------------- ` for (const def of objectDefs) { code += `export type ${toPascalCase(def.name)}Resource = Resource<'${def.name}'>;\n` } code += '\nexport const permissions = {\n' for (const def of objectDefs) { if (def.relations.length === 0 && def.permissions.length === 0) { continue } code += ` ${toCamelCase(def.name)}: {\n` // Generate grant/revoke operations code += generateGrantRevoke(def) // Generate check operations code += generateCheck(def) // Generate find operations code += generateFind(def) code += ` },\n` } code += '};\n' return code } function generateGrantRevoke(def: AugmentedObjectTypeDefinition): string { const resourceType = `${toPascalCase(def.name)}Resource` let code = ' grant: {\n' for (const rel of def.relations) { const subjectTypeLiterals = [ ...new Set(rel.types.map(t => `'${t.typeName}'`)), ].join(' | ') const subjectTypes = `Subject<${subjectTypeLiterals || 'never'}>` code += ` ${toCamelCase(rel.name)}: (subject: ${subjectTypes}, resource: ${resourceType}) => PermissionOperations.grant('${rel.name}').subject(subject).resource(resource),\n` } code += ' },\n' code += ' revoke: {\n' for (const rel of def.relations) { const subjectTypeLiterals = [ ...new Set(rel.types.map(t => `'${t.typeName}'`)), ].join(' | ') const subjectTypes = `Subject<${subjectTypeLiterals || 'never'}>` code += ` ${toCamelCase(rel.name)}: (subject: ${subjectTypes}, resource: ${resourceType}) => PermissionOperations.revoke('${rel.name}').subject(subject).resource(resource),\n` } code += ' },\n' return code } function generateFind(def: AugmentedObjectTypeDefinition): string { let code = ' find: {\n' for (const rel of def.relations) { const pascalRel = toPascalCase(rel.name) const subjectTypeLiterals = [ ...new Set(rel.types.map(t => `'${t.typeName}'`)), ].join(' | ') const subjectTypes = `Subject<${subjectTypeLiterals || 'never'}>` code += ` by${pascalRel}: (subject: ${subjectTypes}) => PermissionOperations.find().relation('${rel.name}').subject(subject),\n` } code += ' },\n' return code } function generateCheck(def: AugmentedObjectTypeDefinition): string { const resourceType = `${toPascalCase(def.name)}Resource` let code = ' check: {\n' for (const perm of def.permissions) { const subjectTypeLiterals = [ ...new Set(perm.inferredSubjectTypes?.map(t => `'${t.typeName}'`)), ].join(' | ') const subjectTypes = `Subject<${subjectTypeLiterals || 'never'}>` code += ` ${toCamelCase(perm.name)}: (subject: ${subjectTypes}, resource: ${resourceType}) => PermissionOperations.check('${perm.name}').subject(subject).resource(resource),\n` } code += ' },\n' return code } function toPascalCase(name: string): string { return name .split(/[_\-\s]+/) .filter(Boolean) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join('') } function toCamelCase(name: string): string { if (name.length === 0) return name return ( toPascalCase(name).charAt(0).toLowerCase() + toPascalCase(name).slice(1) ) }