@schoolai/spicedb-zed-schema-parser
Version:
SpiceDB .zed file format parser and analyzer written in Typescript
122 lines (105 loc) • 4.11 kB
text/typescript
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)
)
}