@schoolai/spicedb-zed-schema-parser
Version:
SpiceDB .zed file format parser and analyzer written in Typescript
1,619 lines (1,599 loc) • 65.2 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Operations: () => Operations,
PermissionOperations: () => PermissionOperations,
Permissions: () => Permissions,
SemanticAnalyzer: () => SemanticAnalyzer,
SpiceDBLexer: () => SpiceDBLexer,
SpiceDBParser: () => SpiceDBParser,
SpiceDBVisitor: () => SpiceDBVisitor,
analyzeSpiceDbSchema: () => analyzeSpiceDbSchema,
createPermissions: () => createPermissions,
generateSDK: () => generateSDK,
parseSpiceDBSchema: () => parseSpiceDBSchema,
parserInstance: () => parserInstance
});
module.exports = __toCommonJS(index_exports);
// src/builder/check.ts
var import_authzed_node = require("@authzed/authzed-node");
// src/builder/types.ts
function parseReference(ref) {
const [part1, part2] = ref.split(":");
if (!part1 || !part2) {
throw new Error(
`Invalid reference format: ${ref}. Expected format: type:id`
);
}
return [part1, part2];
}
// src/builder/check.ts
var CheckOperation = class {
constructor(permission) {
this.permission = permission;
__publicField(this, "subjectRef");
__publicField(this, "resourceRef");
__publicField(this, "consistency");
}
subject(ref) {
this.subjectRef = ref;
return this;
}
resource(ref) {
this.resourceRef = ref;
return this;
}
withConsistency(token) {
this.consistency = import_authzed_node.v1.Consistency.create({
requirement: {
oneofKind: "atLeastAsFresh",
atLeastAsFresh: import_authzed_node.v1.ZedToken.create({ token })
}
});
return this;
}
async execute(client) {
if (!this.subjectRef || !this.resourceRef) {
throw new Error("Check operation requires both subject and resource");
}
const [subjectType, subjectId] = parseReference(this.subjectRef);
const [resourceType, resourceId] = parseReference(this.resourceRef);
const request = import_authzed_node.v1.CheckPermissionRequest.create({
resource: import_authzed_node.v1.ObjectReference.create({
objectType: resourceType,
objectId: resourceId
}),
permission: this.permission,
subject: import_authzed_node.v1.SubjectReference.create({
object: import_authzed_node.v1.ObjectReference.create({
objectType: subjectType,
objectId: subjectId
})
}),
consistency: this.consistency
});
const response = await client.checkPermission(request);
return response.permissionship === import_authzed_node.v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION;
}
toJSON() {
return {
permission: this.permission,
subject: this.subjectRef,
resource: this.resourceRef,
consistency: this.consistency
};
}
};
var BoundCheckOperation = class extends CheckOperation {
constructor(client, permission) {
super(permission);
this.client = client;
}
async execute() {
return super.execute(this.client);
}
};
// src/builder/delete.ts
var import_authzed_node2 = require("@authzed/authzed-node");
var DeleteOperation = class {
constructor() {
__publicField(this, "filter", {});
}
subject(ref) {
const [type, id] = parseReference(ref);
this.filter.subjectType = type;
this.filter.subjectId = id;
return this;
}
relation(rel) {
this.filter.relation = rel;
return this;
}
resource(ref) {
const [type, id] = parseReference(ref);
this.filter.resourceType = type;
this.filter.resourceId = id;
return this;
}
where(filter) {
this.filter = { ...this.filter, ...filter };
return this;
}
async execute(client) {
const relationshipFilter = {};
if (this.filter.resourceType) {
relationshipFilter.resourceType = this.filter.resourceType;
}
if (this.filter.resourceId) {
relationshipFilter.optionalResourceId = this.filter.resourceId;
}
if (this.filter.relation) {
relationshipFilter.optionalRelation = this.filter.relation;
}
if (this.filter.subjectType || this.filter.subjectId) {
const subjectFilter = {};
if (this.filter.subjectType) {
subjectFilter.subjectType = this.filter.subjectType;
}
if (this.filter.subjectId) {
subjectFilter.optionalSubjectId = this.filter.subjectId;
}
relationshipFilter.optionalSubjectFilter = import_authzed_node2.v1.SubjectFilter.create(subjectFilter);
}
const request = import_authzed_node2.v1.DeleteRelationshipsRequest.create({
relationshipFilter: import_authzed_node2.v1.RelationshipFilter.create(relationshipFilter)
});
const response = await client.deleteRelationships(request);
return response.deletedAt?.token || null;
}
toJSON() {
return {
filter: this.filter
};
}
};
var BoundDeleteOperation = class extends DeleteOperation {
constructor(client) {
super();
this.client = client;
}
async execute() {
return super.execute(this.client);
}
};
// src/builder/lookup.ts
var import_authzed_node3 = require("@authzed/authzed-node");
var LookupOperation = class {
constructor() {
__publicField(this, "lookupType");
__publicField(this, "resourceFilter");
__publicField(this, "subjectFilter");
__publicField(this, "permission");
__publicField(this, "consistency");
}
resourcesAccessibleBy(subjectRef) {
this.lookupType = "resources";
const [type, id] = parseReference(subjectRef);
this.subjectFilter = { type, id };
return this;
}
subjectsWithAccessTo(resourceRef) {
this.lookupType = "subjects";
const [type, id] = parseReference(resourceRef);
this.resourceFilter = { type, id };
return this;
}
ofType(type) {
if (this.lookupType === "resources") {
this.resourceFilter = { type };
} else if (this.lookupType === "subjects") {
this.subjectFilter = { type };
}
return this;
}
withPermission(permission) {
this.permission = permission;
return this;
}
withConsistency(token) {
this.consistency = import_authzed_node3.v1.Consistency.create({
requirement: {
oneofKind: "atLeastAsFresh",
atLeastAsFresh: import_authzed_node3.v1.ZedToken.create({ token })
}
});
return this;
}
async execute(client) {
if (!this.permission) {
throw new Error("Lookup operation requires permission");
}
if (this.lookupType === "resources" && this.subjectFilter) {
const request = import_authzed_node3.v1.LookupResourcesRequest.create({
resourceObjectType: this.resourceFilter?.type || "document",
permission: this.permission,
subject: import_authzed_node3.v1.SubjectReference.create({
object: import_authzed_node3.v1.ObjectReference.create({
objectType: this.subjectFilter.type,
objectId: this.subjectFilter.id
})
}),
consistency: this.consistency
});
const stream = await client.lookupResources(request);
const results = [];
for (const result of stream) {
if (result.resourceObjectId) {
results.push({
type: this.resourceFilter?.type || "document",
id: result.resourceObjectId,
permissionship: result.permissionship
});
}
}
return results;
}
if (this.lookupType === "subjects" && this.resourceFilter?.id) {
const request = import_authzed_node3.v1.LookupSubjectsRequest.create({
resource: import_authzed_node3.v1.ObjectReference.create({
objectType: this.resourceFilter.type,
objectId: this.resourceFilter.id
}),
permission: this.permission,
subjectObjectType: this.subjectFilter?.type || "user",
consistency: this.consistency
});
const stream = await client.lookupSubjects(request);
const results = [];
for (const result of stream) {
if (result.subject?.subjectObjectId) {
results.push({
type: this.subjectFilter?.type || "user",
id: result.subject.subjectObjectId,
permissionship: result.subject.permissionship
});
}
}
return results;
}
throw new Error("Invalid lookup configuration");
}
/**
* Special helper for looking up subjects with multiple permission levels
*/
async withPermissions(permissions, client) {
if (!this.resourceFilter?.id) {
throw new Error("Multiple permission lookup requires a specific resource");
}
if (!client) {
throw new Error(
"withPermissions requires a client. Use execute(client) or pass client as second parameter."
);
}
const resultMap = /* @__PURE__ */ new Map();
for (const permission of permissions) {
const request = import_authzed_node3.v1.LookupSubjectsRequest.create({
resource: import_authzed_node3.v1.ObjectReference.create({
objectType: this.resourceFilter.type,
objectId: this.resourceFilter.id
}),
permission,
subjectObjectType: this.subjectFilter?.type || "user",
consistency: this.consistency
});
const stream = await client.lookupSubjects(request);
for (const result of stream) {
if (result.subject?.subjectObjectId) {
if (!resultMap.has(result.subject.subjectObjectId)) {
resultMap.set(result.subject.subjectObjectId, permission);
}
}
}
}
return resultMap;
}
toJSON() {
return {
lookupType: this.lookupType,
resourceFilter: this.resourceFilter,
subjectFilter: this.subjectFilter,
permission: this.permission,
consistency: this.consistency
};
}
};
var BoundLookupOperation = class extends LookupOperation {
constructor(client) {
super();
this.client = client;
}
async execute() {
return super.execute(this.client);
}
async withPermissions(permissions) {
return super.withPermissions(permissions, this.client);
}
};
// src/builder/query.ts
var import_authzed_node4 = require("@authzed/authzed-node");
var QueryOperation = class {
constructor() {
__publicField(this, "filter", {});
__publicField(this, "queryType");
__publicField(this, "permission");
__publicField(this, "consistency");
}
subjects(type) {
this.queryType = "subjects";
if (type) this.filter.subjectType = type;
return this;
}
resources(type) {
this.queryType = "resources";
if (type) this.filter.resourceType = type;
return this;
}
subject(ref) {
if (ref.includes("*")) {
const [type] = parseReference(ref);
this.filter.subjectType = type;
} else {
const [type, id] = parseReference(ref);
this.filter.subjectType = type;
this.filter.subjectId = id;
}
return this;
}
relation(rel) {
if (rel !== "*") {
this.filter.relation = rel;
}
return this;
}
resource(ref) {
if (ref.includes("*")) {
const [type] = parseReference(ref);
this.filter.resourceType = type;
} else {
const [type, id] = parseReference(ref);
this.filter.resourceType = type;
this.filter.resourceId = id;
}
return this;
}
withPermission(permission) {
this.permission = permission;
return this;
}
withConsistency(token) {
this.consistency = import_authzed_node4.v1.Consistency.create({
requirement: {
oneofKind: "atLeastAsFresh",
atLeastAsFresh: import_authzed_node4.v1.ZedToken.create({ token })
}
});
return this;
}
async execute(client) {
if (this.queryType === "subjects" && this.filter.resourceType && this.filter.resourceId && this.permission) {
const request2 = import_authzed_node4.v1.LookupSubjectsRequest.create({
resource: import_authzed_node4.v1.ObjectReference.create({
objectType: this.filter.resourceType,
objectId: this.filter.resourceId
}),
permission: this.permission,
subjectObjectType: this.filter.subjectType || "user",
consistency: this.consistency
});
const stream2 = await client.lookupSubjects(request2);
const results2 = [];
for (const result of stream2) {
if (result.subject?.subjectObjectId) {
results2.push({
type: this.filter.subjectType || "user",
id: result.subject.subjectObjectId,
relation: result.subject.permissionship === import_authzed_node4.v1.LookupPermissionship.HAS_PERMISSION ? this.permission : void 0
});
}
}
return results2;
}
if (this.queryType === "resources" && this.filter.subjectType && this.filter.subjectId && this.permission) {
const request2 = import_authzed_node4.v1.LookupResourcesRequest.create({
resourceObjectType: this.filter.resourceType || "document",
permission: this.permission,
subject: import_authzed_node4.v1.SubjectReference.create({
object: import_authzed_node4.v1.ObjectReference.create({
objectType: this.filter.subjectType,
objectId: this.filter.subjectId
})
}),
consistency: this.consistency
});
const stream2 = await client.lookupResources(request2);
const results2 = [];
for (const result of stream2) {
if (result.resourceObjectId) {
results2.push({
type: this.filter.resourceType || "document",
id: result.resourceObjectId,
permissionship: result.permissionship
});
}
}
return results2;
}
const filter = {};
if (this.filter.resourceType) {
filter.resourceType = this.filter.resourceType;
}
if (this.filter.resourceId) {
filter.optionalResourceId = this.filter.resourceId;
}
if (this.filter.relation) {
filter.optionalRelation = this.filter.relation;
}
if (this.filter.subjectType || this.filter.subjectId) {
const subjectFilter = {};
if (this.filter.subjectType) {
subjectFilter.subjectType = this.filter.subjectType;
}
if (this.filter.subjectId) {
subjectFilter.optionalSubjectId = this.filter.subjectId;
}
filter.optionalSubjectFilter = import_authzed_node4.v1.SubjectFilter.create(subjectFilter);
}
const request = import_authzed_node4.v1.ReadRelationshipsRequest.create({
relationshipFilter: import_authzed_node4.v1.RelationshipFilter.create(filter),
consistency: this.consistency
});
const stream = await client.readRelationships(request);
const results = [];
for (const result of stream) {
if (result.relationship) {
results.push({
type: result.relationship.resource?.objectType || "",
id: result.relationship.resource?.objectId || "",
relation: result.relationship.relation,
subjectType: result.relationship.subject?.object?.objectType || "",
subjectId: result.relationship.subject?.object?.objectId || ""
});
}
}
return results;
}
toJSON() {
return {
queryType: this.queryType,
filter: this.filter,
permission: this.permission,
consistency: this.consistency
};
}
};
var BoundQueryOperation = class extends QueryOperation {
constructor(client) {
super();
this.client = client;
}
async execute() {
return super.execute(this.client);
}
};
// src/builder/transaction.ts
var import_authzed_node5 = require("@authzed/authzed-node");
var Transaction = class {
constructor() {
__publicField(this, "operations", []);
}
grant(relation) {
return new TransactionWriteOperation(this, "grant", relation);
}
revoke(relation) {
return new TransactionWriteOperation(this, "revoke", relation);
}
add(operation) {
this.operations.push(operation);
return this;
}
async execute(client) {
const updates = this.operations.map((op) => op());
const request = import_authzed_node5.v1.WriteRelationshipsRequest.create({ updates });
const response = await client.writeRelationships(request);
return {
token: response.writtenAt?.token || null,
succeeded: true,
operationCount: updates.length
};
}
toJSON() {
return {
operationCount: this.operations.length
};
}
};
var BoundTransaction = class extends Transaction {
constructor(client) {
super();
this.client = client;
}
grant(relation) {
return new TransactionWriteOperation(this, "grant", relation);
}
revoke(relation) {
return new TransactionWriteOperation(this, "revoke", relation);
}
async execute() {
return super.execute(this.client);
}
async commit() {
return this.execute();
}
};
var TransactionWriteOperation = class {
constructor(transaction, operation, relation) {
this.transaction = transaction;
this.operation = operation;
this.relation = relation;
__publicField(this, "subjects", []);
__publicField(this, "resources", []);
}
subject(ref) {
this.subjects = Array.isArray(ref) ? ref : [ref];
return this;
}
resource(ref) {
this.resources = Array.isArray(ref) ? ref : [ref];
return this;
}
and() {
for (const subjectRef of this.subjects) {
for (const resourceRef of this.resources) {
this.transaction.add(() => {
const [subjectType, subjectId] = parseReference(subjectRef);
const [resourceType, resourceId] = parseReference(resourceRef);
const relationship = import_authzed_node5.v1.Relationship.create({
resource: import_authzed_node5.v1.ObjectReference.create({
objectType: resourceType,
objectId: resourceId
}),
relation: this.relation,
subject: import_authzed_node5.v1.SubjectReference.create({
object: import_authzed_node5.v1.ObjectReference.create({
objectType: subjectType,
objectId: subjectId
})
})
});
return import_authzed_node5.v1.RelationshipUpdate.create({
relationship,
operation: this.operation === "grant" ? import_authzed_node5.v1.RelationshipUpdate_Operation.TOUCH : import_authzed_node5.v1.RelationshipUpdate_Operation.DELETE
});
});
}
}
return this.transaction;
}
};
// src/builder/write.ts
var import_authzed_node6 = require("@authzed/authzed-node");
var WriteOperation = class {
constructor(operation, relation) {
this.operation = operation;
this.relation = relation;
__publicField(this, "subjects", []);
__publicField(this, "resources", []);
__publicField(this, "consistency");
}
subject(ref) {
this.subjects = Array.isArray(ref) ? ref : [ref];
return this;
}
resource(ref) {
this.resources = Array.isArray(ref) ? ref : [ref];
return this;
}
withConsistency(token) {
this.consistency = import_authzed_node6.v1.Consistency.create({
requirement: {
oneofKind: "atLeastAsFresh",
atLeastAsFresh: import_authzed_node6.v1.ZedToken.create({ token })
}
});
return this;
}
async execute(client) {
const updates = [];
for (const subjectRef of this.subjects) {
for (const resourceRef of this.resources) {
const [subjectType, subjectId] = parseReference(subjectRef);
const [resourceType, resourceId] = parseReference(resourceRef);
const relationship = import_authzed_node6.v1.Relationship.create({
resource: import_authzed_node6.v1.ObjectReference.create({
objectType: resourceType,
objectId: resourceId
}),
relation: this.relation,
subject: import_authzed_node6.v1.SubjectReference.create({
object: import_authzed_node6.v1.ObjectReference.create({
objectType: subjectType,
objectId: subjectId
})
})
});
updates.push(
import_authzed_node6.v1.RelationshipUpdate.create({
relationship,
operation: this.operation === "grant" ? import_authzed_node6.v1.RelationshipUpdate_Operation.TOUCH : import_authzed_node6.v1.RelationshipUpdate_Operation.DELETE
})
);
}
}
const request = import_authzed_node6.v1.WriteRelationshipsRequest.create({ updates });
const response = await client.writeRelationships(request);
return response.writtenAt?.token || null;
}
/**
* Convert to a plain object for serialization
*/
toJSON() {
return {
operation: this.operation,
relation: this.relation,
subjects: this.subjects,
resources: this.resources,
consistency: this.consistency
};
}
};
var BoundWriteOperation = class extends WriteOperation {
constructor(client, operation, relation) {
super(operation, relation);
this.client = client;
}
async execute() {
return super.execute(this.client);
}
};
// src/builder/operations.ts
var PermissionOperations = class {
/**
* Create a grant operation
*/
static grant(relation) {
return new WriteOperation("grant", relation);
}
/**
* Create a revoke operation
*/
static revoke(relation) {
return new WriteOperation("revoke", relation);
}
/**
* Create a check operation
*/
static check(permission) {
return new CheckOperation(permission);
}
/**
* Create a find/query operation
*/
static find() {
return new QueryOperation();
}
/**
* Create a delete operation
*/
static delete() {
return new DeleteOperation();
}
/**
* Create a batch transaction
*/
static batch() {
return new Transaction();
}
/**
* Create a lookup operation
*/
static lookup() {
return new LookupOperation();
}
};
var Permissions = class {
constructor(client) {
this.client = client;
}
/**
* Grant a relation between subjects and resources
*/
grant(relation) {
return new BoundWriteOperation(this.client, "grant", relation);
}
/**
* Revoke a relation between subjects and resources
*/
revoke(relation) {
return new BoundWriteOperation(this.client, "revoke", relation);
}
/**
* Check if a subject has a permission on a resource
*/
check(permission) {
return new BoundCheckOperation(this.client, permission);
}
/**
* Find subjects or resources matching criteria
*/
find() {
return new BoundQueryOperation(this.client);
}
/**
* Delete relationships matching a filter
*/
delete() {
return new BoundDeleteOperation(this.client);
}
/**
* Create a batch transaction
*/
batch() {
return new BoundTransaction(this.client);
}
/**
* Lookup resources accessible to a subject
*/
lookup() {
return new BoundLookupOperation(this.client);
}
/**
* Execute a pure operation with this instance's client
*/
async execute(operation) {
return operation.execute(this.client);
}
};
function createPermissions(client) {
return new Permissions(client);
}
var Operations = PermissionOperations;
// src/generate-sdk.ts
function generateSDK(schema, parserImport = "@schoolai/spicedb-zed-schema-parser") {
const objectDefs = schema.definitions.filter(
(def) => 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}'>;
`;
}
code += "\nexport const permissions = {\n";
for (const def of objectDefs) {
if (def.relations.length === 0 && def.permissions.length === 0) {
continue;
}
code += ` ${toCamelCase(def.name)}: {
`;
code += generateGrantRevoke(def);
code += generateCheck(def);
code += generateFind(def);
code += ` },
`;
}
code += "};\n";
return code;
}
function generateGrantRevoke(def) {
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),
`;
}
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),
`;
}
code += " },\n";
return code;
}
function generateFind(def) {
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),
`;
}
code += " },\n";
return code;
}
function generateCheck(def) {
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),
`;
}
code += " },\n";
return code;
}
function toPascalCase(name) {
return name.split(/[_\-\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
}
function toCamelCase(name) {
if (name.length === 0) return name;
return toPascalCase(name).charAt(0).toLowerCase() + toPascalCase(name).slice(1);
}
// src/semantic-analyzer/dependency-graph.ts
var DependencyGraph = class {
constructor() {
__publicField(this, "adjacencyList", /* @__PURE__ */ new Map());
__publicField(this, "nodes", /* @__PURE__ */ new Map());
}
addNode(node) {
this.nodes.set(node.fullName, node);
if (!this.adjacencyList.has(node.fullName)) {
this.adjacencyList.set(node.fullName, /* @__PURE__ */ new Set());
}
}
addEdge(from, to) {
if (!this.adjacencyList.has(from)) {
this.adjacencyList.set(from, /* @__PURE__ */ new Set());
}
this.adjacencyList.get(from).add(to);
}
// Find cycles via depth-first search
findCycles() {
const cycles = [];
const visited = /* @__PURE__ */ new Set();
const recursionStack = /* @__PURE__ */ new Set();
const path = [];
const dfs = (node) => {
visited.add(node);
recursionStack.add(node);
path.push(node);
const neighbors = this.adjacencyList.get(node) || /* @__PURE__ */ new Set();
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
dfs(neighbor);
} else if (recursionStack.has(neighbor)) {
const cycleStart = path.indexOf(neighbor);
cycles.push(path.slice(cycleStart).concat(neighbor));
}
}
path.pop();
recursionStack.delete(node);
};
for (const node of this.adjacencyList.keys()) {
if (!visited.has(node)) {
dfs(node);
}
}
return cycles;
}
};
// src/semantic-analyzer/symbol-table.ts
var SymbolTable = class {
constructor() {
__publicField(this, "symbols", /* @__PURE__ */ new Map());
}
addDefinition(def) {
const info = {
type: def.type === "definition" ? "definition" : "caveat",
definition: def,
relations: /* @__PURE__ */ new Map(),
permissions: /* @__PURE__ */ new Map()
};
if (def.type === "definition") {
for (const rel of def.relations) {
info.relations.set(rel.name, rel);
}
for (const perm of def.permissions) {
info.permissions.set(perm.name, perm);
}
}
this.symbols.set(def.name, info);
}
getDefinition(name) {
return this.symbols.get(name);
}
hasDefinition(name) {
return this.symbols.has(name);
}
getAllDefinitions() {
return Array.from(this.symbols.values());
}
getRelation(typeName, relationName) {
const info = this.symbols.get(typeName);
return info?.relations.get(relationName);
}
getPermission(typeName, permissionName) {
const info = this.symbols.get(typeName);
return info?.permissions.get(permissionName);
}
hasRelationOrPermission(typeName, name) {
const info = this.symbols.get(typeName);
if (!info) return false;
return info.relations.has(name) || info.permissions.has(name);
}
};
// src/semantic-analyzer/type-inference.ts
var TypeInferenceEngine = class {
constructor(symbolTable) {
this.symbolTable = symbolTable;
}
inferExpressionType(defName, expr, callStack = /* @__PURE__ */ new Set()) {
switch (expr.type) {
case "identifier": {
const rel = this.symbolTable.getRelation(defName, expr.name);
if (rel) {
const resolvedTypes = [];
for (const typeRef of rel.types) {
if (typeRef.relation) {
const subjectRelationTypes = this.inferSubjectRelationType(
typeRef.typeName,
typeRef.relation,
callStack
);
if (subjectRelationTypes) {
resolvedTypes.push(...subjectRelationTypes);
}
} else {
resolvedTypes.push(typeRef);
}
}
return this.deduplicateTypes(resolvedTypes);
}
const perm = this.symbolTable.getPermission(defName, expr.name);
if (perm) {
const stackKey = `${defName}#${expr.name}`;
if (callStack.has(stackKey)) {
return null;
}
callStack.add(stackKey);
const result = this.inferExpressionType(
defName,
perm.expression,
callStack
);
callStack.delete(stackKey);
return result;
}
return null;
}
case "union": {
const allTypes = [];
for (const operand of expr.operands) {
const types = this.inferExpressionType(defName, operand, callStack);
if (types) {
allTypes.push(...types);
}
}
return this.deduplicateTypes(allTypes);
}
case "intersection": {
let commonTypes = null;
for (const operand of expr.operands) {
const types = this.inferExpressionType(defName, operand, callStack);
if (!types) return null;
if (!commonTypes) {
commonTypes = types;
} else {
commonTypes = this.intersectTypes(commonTypes, types);
}
}
return commonTypes;
}
case "exclusion":
return this.inferExpressionType(defName, expr.left, callStack);
case "arrow":
case "any":
case "all": {
const leftTypes = this.inferExpressionType(
defName,
expr.left,
callStack
);
if (!leftTypes) return null;
const resultTypes = [];
for (const leftType of leftTypes) {
const targetRel = this.symbolTable.getRelation(
leftType.typeName,
expr.target
);
if (targetRel) {
for (const typeRef of targetRel.types) {
if (typeRef.relation) {
const subjectRelationTypes = this.inferSubjectRelationType(
typeRef.typeName,
typeRef.relation,
callStack
);
if (subjectRelationTypes) {
resultTypes.push(...subjectRelationTypes);
}
} else {
resultTypes.push(typeRef);
}
}
}
const targetPerm = this.symbolTable.getPermission(
leftType.typeName,
expr.target
);
if (targetPerm) {
const stackKey = `${leftType.typeName}#${expr.target}`;
if (callStack.has(stackKey)) {
continue;
}
callStack.add(stackKey);
const permTypes = this.inferExpressionType(
leftType.typeName,
targetPerm.expression,
callStack
);
callStack.delete(stackKey);
if (permTypes) {
resultTypes.push(...permTypes);
}
}
}
return this.deduplicateTypes(resultTypes);
}
default:
return null;
}
}
inferSubjectRelationType(typeName, relationName, callStack) {
const targetRel = this.symbolTable.getRelation(typeName, relationName);
if (targetRel) {
const resolvedTypes = [];
for (const typeRef of targetRel.types) {
if (typeRef.relation) {
const subjectRelationTypes = this.inferSubjectRelationType(
typeRef.typeName,
typeRef.relation,
callStack
);
if (subjectRelationTypes) {
resolvedTypes.push(...subjectRelationTypes);
}
} else {
resolvedTypes.push(typeRef);
}
}
return this.deduplicateTypes(resolvedTypes);
}
const targetPerm = this.symbolTable.getPermission(typeName, relationName);
if (targetPerm) {
const stackKey = `${typeName}#${relationName}`;
if (callStack.has(stackKey)) {
return null;
}
callStack.add(stackKey);
const result = this.inferExpressionType(
typeName,
targetPerm.expression,
callStack
);
callStack.delete(stackKey);
return result;
}
return null;
}
deduplicateTypes(types) {
const seen = /* @__PURE__ */ new Set();
const result = [];
for (const type of types) {
const key = `${type.typeName}${type.wildcard ? ":*" : ""}${type.relation ? "#" + type.relation : ""}`;
if (!seen.has(key)) {
seen.add(key);
result.push(type);
}
}
return result;
}
intersectTypes(a, b) {
const result = [];
const bKeys = new Set(
b.map(
(type) => `${type.typeName}${type.wildcard ? ":*" : ""}${type.relation ? "#" + type.relation : ""}`
)
);
for (const typeA of a) {
const keyA = `${typeA.typeName}${typeA.wildcard ? ":*" : ""}${typeA.relation ? "#" + typeA.relation : ""}`;
if (bKeys.has(keyA)) {
result.push(typeA);
}
}
return this.deduplicateTypes(result);
}
};
// src/semantic-analyzer/analyzer.ts
var criticalPreAugmentationErrorCodes = [
"DUPLICATE_DEFINITION",
"UNDEFINED_TYPE",
"UNDEFINED_RELATION",
"DUPLICATE_MEMBER_NAME"
];
var fatalForUsageErrorCodes = [
...criticalPreAugmentationErrorCodes,
// Errors caught before augmentation
"CIRCULAR_DEPENDENCY",
// Cycles make the graph unusable
"UNDEFINED_IDENTIFIER",
// Referenced name doesn't exist
"UNDEFINED_ARROW_TARGET",
// Arrow target doesn't exist on resolved types
"AUGMENTATION_INTERNAL_ERROR"
// If augmentation process itself failed
];
var SemanticAnalyzer = class {
constructor() {
__publicField(this, "symbolTable");
__publicField(this, "typeInference");
__publicField(this, "errors", []);
__publicField(this, "warnings", []);
this.symbolTable = new SymbolTable();
this.typeInference = new TypeInferenceEngine(this.symbolTable);
}
analyze(ast) {
this.errors = [];
this.warnings = [];
this.symbolTable = new SymbolTable();
this.typeInference = new TypeInferenceEngine(this.symbolTable);
this.buildSymbolTable(ast);
this.validateDefinitions(ast);
this.checkForCycles(ast);
this.validateExpressions(ast);
this.performAdditionalChecks(ast);
const isValid = this.errors.length === 0;
const isFatalForUsage = this.errors.some(
(e) => fatalForUsageErrorCodes.includes(e.code)
);
const augmentedAst = this.augmentAst(ast);
if (isFatalForUsage) {
return {
augmentedAst: void 0,
symbolTable: this.symbolTable,
// Always return the symbol table
errors: this.errors,
warnings: this.warnings,
isValid
};
}
return {
augmentedAst,
symbolTable: this.symbolTable,
// Always return the symbol table
errors: this.errors,
warnings: this.warnings,
isValid
};
}
augmentAst(ast) {
try {
const augmentedDefinitions = [];
for (const def of ast.definitions) {
if (def.type === "definition") {
const augmentedRelations = def.relations.map((rel) => ({
...rel
}));
const augmentedPermissions = [];
for (const perm of def.permissions) {
const inferredTypes = this.typeInference.inferExpressionType(
def.name,
perm.expression
);
augmentedPermissions.push({
...perm,
inferredSubjectTypes: inferredTypes
});
}
const augmentedDef = {
type: "definition",
// Explicitly set type
name: def.name,
relations: augmentedRelations,
permissions: augmentedPermissions,
// Safely access comments, assuming ObjectTypeDefinition might have 'comments'
// even if not in its strict imported type, consistent with BaseNode expectation.
comments: def.comments
};
augmentedDefinitions.push(augmentedDef);
} else if (def.type === "caveat") {
augmentedDefinitions.push(def);
}
}
return { definitions: augmentedDefinitions };
} catch (e) {
this.addError(
"AUGMENTATION_INTERNAL_ERROR",
`Internal error during AST augmentation: ${e.message}`,
{}
);
return void 0;
}
}
// Phase 1: Build symbol table
buildSymbolTable(ast) {
const definedNames = /* @__PURE__ */ new Set();
for (const def of ast.definitions) {
if (definedNames.has(def.name)) {
this.addError(
"DUPLICATE_DEFINITION",
`Duplicate definition name: ${def.name}`,
{ definition: def.name }
);
}
definedNames.add(def.name);
this.symbolTable.addDefinition(def);
}
}
// Phase 2: Validate definitions
validateDefinitions(ast) {
for (const def of ast.definitions) {
if (def.type === "definition") {
this.validateObjectTypeDefinition(def);
} else if (def.type === "caveat") {
this.validateCaveatDefinition(def);
}
}
}
validateObjectTypeDefinition(def) {
const names = /* @__PURE__ */ new Set();
for (const rel of def.relations) {
if (names.has(rel.name)) {
this.addError(
"DUPLICATE_MEMBER_NAME",
`Duplicate relation/permission name '${rel.name}' in ${def.name}`,
{ definition: def.name, relation: rel.name }
);
}
names.add(rel.name);
this.validateRelation(def.name, rel);
}
for (const perm of def.permissions) {
if (names.has(perm.name)) {
this.addError(
"DUPLICATE_MEMBER_NAME",
`Duplicate relation/permission name '${perm.name}' in ${def.name}`,
{ definition: def.name, permission: perm.name }
);
}
names.add(perm.name);
}
}
validateRelation(defName, rel) {
for (const type of rel.types) {
if (!this.symbolTable.hasDefinition(type.typeName)) {
this.addError(
"UNDEFINED_TYPE",
`Undefined type '${type.typeName}' in relation '${rel.name}'`,
{ definition: defName, relation: rel.name }
);
}
if (type.relation) {
const targetType = this.symbolTable.getDefinition(type.typeName);
if (targetType && targetType.type === "definition") {
if (!this.symbolTable.hasRelationOrPermission(
type.typeName,
type.relation
)) {
this.addError(
"UNDEFINED_RELATION",
`Undefined relation '${type.relation}' on type '${type.typeName}'`,
{ definition: defName, relation: rel.name }
);
}
}
}
if (type.wildcard) {
this.addWarning(
"WILDCARD_USAGE",
`Wildcard used in relation '${rel.name}'. Be careful with public access.`,
{ definition: defName, relation: rel.name }
);
}
}
}
validateCaveatDefinition(def) {
const validTypes = [
"int",
"uint",
"string",
"bool",
"bytes",
"list",
"map",
"timestamp",
"duration"
];
for (const param of def.parameters) {
if (!validTypes.includes(param.type)) {
this.addError(
"INVALID_PARAMETER_TYPE",
`Invalid parameter type '${param.type}' in caveat '${def.name}'`,
{ definition: def.name }
);
}
}
const paramNames = new Set(def.parameters.map((p) => p.name));
if (!paramNames.has(def.expression.left)) {
this.addError(
"UNDEFINED_CAVEAT_PARAMETER",
`Unknown parameter '${def.expression.left}' in caveat expression`,
{ definition: def.name }
);
}
}
// Phase 3: Check for cycles
checkForCycles(ast) {
const graph = new DependencyGraph();
for (const def of ast.definitions) {
if (def.type === "definition") {
for (const perm of def.permissions) {
const fromNode = `${def.name}#${perm.name}`;
graph.addNode({
type: "permission",
name: perm.name,
fullName: fromNode
});
this.addExpressionDependencies(
graph,
def.name,
perm.name,
perm.expression
);
}
}
}
const cycles = graph.findCycles();
for (const cycle of cycles) {
if (cycle.length === 2 && cycle[0] === cycle[1]) {
continue;
}
this.addError(
"CIRCULAR_DEPENDENCY",
`Circular dependency detected: ${cycle.join(" -> ")}`,
{}
);
}
}
addExpressionDependencies(graph, defName, permName, expr) {
const fromNode = `${defName}#${permName}`;
switch (expr.type) {
case "identifier":
if (this.symbolTable.hasRelationOrPermission(defName, expr.name)) {
const toNode = `${defName}#${expr.name}`;
graph.addNode({
type: "relation_or_permission",
name: expr.name,
fullName: toNode
});
graph.addEdge(fromNode, toNode);
}
break;
case "union":
case "intersection":
for (const operand of expr.operands) {
this.addExpressionDependencies(graph, defName, permName, operand);
}
break;
case "exclusion":
this.addExpressionDependencies(graph, defName, permName, expr.left);
this.addExpressionDependencies(graph, defName, permName, expr.right);
break;
case "arrow":
case "any":
case "all": {
const leftTypes = this.typeInference.inferExpressionType(
defName,
expr.left
);
if (leftTypes) {
for (const leftType of leftTypes) {
const toNode = `${leftType.typeName}#${expr.target}`;
graph.addNode({
type: "relation_or_permission",
name: expr.target,
fullName: toNode
});
graph.addEdge(fromNode, toNode);
}
}
this.addExpressionDependencies(graph, defName, permName, expr.left);
break;
}
}
}
// Phase 4: Validate expressions
validateExpressions(ast) {
for (const def of ast.definitions) {
if (def.type === "definition") {
for (const perm of def.permissions) {
this.validateExpression(def.name, perm.expression);
}
}
}
}
validateExpression(defName, expr) {
switch (expr.type) {
case "identifier":
if (!this.symbolTable.hasRelationOrPermission(defName, expr.name)) {
this.addError(
"UNDEFINED_IDENTIFIER",
`Undefined identifier '${expr.name}' in type '${defName}'`,
{ definition: defName }
);
}
break;
case "union":
case "intersection":
if (expr.operands.length < 2) {
this.addError(
"INVALID_EXPRESSION",
`${expr.type} expression must have at least 2 operands`,
{ definition: defName }
);
}
for (const operand of expr.operands) {
this.validateExpression(defName, operand);
}
break;
case "exclusion":
this.validateExpression(defName, expr.left);
this.validateExpression(defName, expr.right);
break;
case "arrow":
case "any":
case "all": {
this.validateExpression(defName, expr.left);
const leftTypes = this.typeInference.inferExpressionType(
defName,
expr.left
);
if (leftTypes) {
let targetFound = false;
for (const leftType of leftTypes) {
if (this.symbolTable.hasRelationOrPermission(
leftType.typeName,
expr.target
)) {
targetFound = true;
break;
}
}
if (!targetFound) {
const resolvedTypeNames = leftTypes.map((t) => t.typeName).join(", ");
this.addError(
"UNDEFINED_ARROW_TARGET",
`Target '${expr.target}' not found on resolved types [${resolvedTypeNames}] for expression starting with '${expr.left.name}'`,
{ definition: defName }
);
}
}
break;
}
}
}
// Phase 5: Additional checks
performAdditionalChecks(ast) {
const usedTypes = /* @__PURE__ */ new Set();
for (const def of ast.definitions) {
if (def.type === "definition") {
for (const rel of def.relations) {
for (const type of rel.types) {
usedTypes.add(type.typeName);
}
}
}
}
for (const def of ast.definitions) {
if (def.type === "definition" && !usedTypes.has(def.name)) {
const isUsed = false;
if (!isUsed && def.name !== "user") {
this.addWarning(
"UNUSED_DEFINITION",
`Definition '${def.name}' is not referenced anywhere`,
{ definition: def.name }
);
}
}
}
for (const def of ast.definitions) {
if (def.type === "definition") {
for (const perm of def.permissions) {
if (this.isEmptyPermission(perm.expression)) {
this.addWarning(
"EMPTY_PERMISSION",
`Permission '${perm.name}' in '${def.name}' has no granting mechanism`,
{ definition: def.name, permission: perm.name }
);
}
}
}
}
}
isEmptyPermission(_expr) {
return false;
}
// Helper methods
addError(code, message, location) {
this.errors.push({
type: "semantic_error",
code,
message,
location
});
}
addWarning(code, message, location) {
this.warnings.push({
type: "semantic_error",
code,
message,
location
});
}
};
function analyzeSpiceDbSchema(ast) {
const analyzer = new SemanticAnalyzer();
return analyzer.analyze(ast);
}
// src/schema-parser/parser.ts
var import_chevrotain = require("chevrotain");
var Identifier = (0, import_chevrotain.createToken)({
name: "Identifier",
pattern: /[a-zA-Z_][a-zA-Z0-9_]*/
});
var Integer = (0, import_chevrotain.createToken)({ name: "Integer", pattern: /0|[1-9]\d*/ });
var StringLiteral = (0, import_chevrotain.createToken)({ name: "StringLiteral", pattern: /"[^"]*"/ });
var Definition = (0, import_chevrotain.createToken)({
name: "Definition",
pattern: /definition/,
longer_alt: Identif