import fs from "node:fs"; import path from "node:path"; import protobuf from "protobufjs"; const CLASS_OPTION = "(camino.class)"; const INTERFACE_OPTION = "(camino.interface)"; const IMPLEMENTS_OPTION = "(camino.implements)"; const CONSTRUCTOR_OPTION = "(camino.constructor)"; const METHOD_OPTION = "(camino.method)"; const MIGRATION_OPTION = "(camino.migration)"; const CONFLICT_OPTION = "(camino.conflict)"; const EDGE_OPTION = "(camino.edge)"; const FIELD_STORAGE_OPTION = "(camino.field_storage)"; const FIELD_OPS_OPTION = "(camino.field_ops)"; const INTERFACE_FIELD_OPTION = "(camino.interface_field)"; const DISPLAY_LABEL_OPTION = "(camino.display_label)"; const IMPL_OPTION = "(camino.impl)"; const SCHEMA_NAMESPACE_OPTION = "(camino.schema_namespace)"; const SCHEMA_VERSION_OPTION = "(camino.schema_version)"; const enumName = (value) => String(value) .replace(/([a-z0-9])([A-Z])/g, "$1_$2") .replace(/-/g, "_") .toUpperCase(); const lowerEnumName = (value) => enumName(value).toLowerCase(); const getOption = (options, name) => { if (!options) { return undefined; } if (Object.hasOwn(options, name)) { return options[name]; } const bareName = name.replace(/^\((.*)\)$/, "$1"); if (Object.hasOwn(options, bareName)) { return options[bareName]; } return undefined; }; const getParsedOptions = (reflection) => { const parsedOptions = reflection .parsedOptions; return Array.isArray(parsedOptions) ? parsedOptions.filter((option) => Boolean(option) && typeof option === "object" && !Array.isArray(option)) : []; }; const getReflectionOptions = (reflection, name) => { const parsedValues = getParsedOptions(reflection).flatMap((option) => { const value = getOption(option, name); return value === undefined ? [] : [value]; }); if (parsedValues.length > 0) { return parsedValues; } const value = getOption(reflection.options, name); return value === undefined ? [] : [value]; }; const getReflectionOption = (reflection, name) => getReflectionOptions(reflection, name)[0]; const asObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined; const asArray = (value) => Array.isArray(value) ? value : value === undefined ? [] : [value]; const readString = (object, key) => { const value = object?.[key]; return typeof value === "string" ? value : undefined; }; const readNumber = (object, key) => { const value = object?.[key]; return typeof value === "number" && Number.isFinite(value) ? value : undefined; }; const readBoolean = (object, key) => { const value = object?.[key]; return typeof value === "boolean" ? value : undefined; }; const symbolRefFromOption = (value, fallback) => { const object = asObject(value); if (!object) { return fallback; } return { namespace: readString(object, "namespace") ?? fallback.namespace, name: readString(object, "name") ?? fallback.name, version: readString(object, "version") ?? fallback.version, ...(readString(object, "hash") ? { hash: readString(object, "hash") } : {}), }; }; const typeRefFromOption = (value, fallbackVersion) => { const object = asObject(value); if (!object) { return undefined; } const symbol = asObject(object.symbol) ? symbolRefFromOption(object.symbol, { namespace: "", name: "", version: fallbackVersion, }) : undefined; const protoType = readString(object, "proto_type")?.replace(/^\./, ""); const symbolProtoType = symbol?.namespace && symbol.name ? `${symbol.namespace}.${symbol.name}` : ""; const resolvedProtoType = protoType ?? symbolProtoType; if (!resolvedProtoType) { return undefined; } return { protoType: resolvedProtoType, ...(symbol?.namespace && symbol.name ? { symbol } : {}), }; }; const interfaceFieldContractFromOption = (value, schemaVersion) => { const object = asObject(value); if (!object) { return undefined; } const type = typeRefFromOption(object.type, schemaVersion); const refTargetTypeParam = readString(object, "ref_target_type_param"); const edgeCardinality = object.edge_cardinality === undefined ? undefined : cardinalityFromOption(object.edge_cardinality); return { required: readBoolean(object, "required") ?? false, readable: readBoolean(object, "readable") ?? false, writable: readBoolean(object, "writable") ?? false, watchable: readBoolean(object, "watchable") ?? false, ...(readString(object, "type_param") ? { typeParam: readString(object, "type_param") } : {}), ...(type ? { type } : {}), ...(refTargetTypeParam ? { refTargetTypeParam } : {}), ...(edgeCardinality ? { edgeCardinality } : {}), }; }; const functionRefFromOption = (value) => { const object = asObject(value); if (!object) { throw new Error("Expected function ref option object"); } const packageNamespace = readString(object, "package_namespace"); const packageName = readString(object, "package_name"); const symbol = readString(object, "symbol"); if (!packageNamespace || !packageName || !symbol) { throw new Error("Function ref requires package_namespace, package_name, and symbol"); } return { packageNamespace, packageName, symbol, ...(readString(object, "operation") ? { operation: readString(object, "operation") } : {}), ...(readString(object, "version_ref") ? { versionRef: readString(object, "version_ref") } : {}), }; }; const conflictFromOption = (value) => { const normalized = enumName(value); if (!value || normalized === "CONFLICT_STRATEGY_UNSPECIFIED") { return "preserve_conflicts"; } if (normalized === "REPLACE") { return "replace"; } if (normalized === "PRESERVE_CONFLICTS") { return "preserve_conflicts"; } if (normalized === "CRDT") { return "crdt"; } throw new Error(`Unknown Camino conflict strategy: ${String(value)}`); }; const cardinalityFromOption = (value) => { const normalized = lowerEnumName(value); if (!value || normalized === "cardinality_unspecified") { return "many"; } if (normalized === "optional_one" || normalized === "exactly_one" || normalized === "many" || normalized === "many_unique" || normalized === "many_ordered" || normalized === "many_unique_ordered") { return normalized; } throw new Error(`Unknown Camino cardinality: ${String(value)}`); }; const fieldStorageKindFromOption = (value) => { if (value === 5) { return "static_final"; } const normalized = lowerEnumName(value); if (!value || normalized === "field_storage_kind_unspecified") { return "stored"; } if (normalized === "stored" || normalized === "derived" || normalized === "lazy" || normalized === "external" || normalized === "static_final") { return normalized; } throw new Error(`Unknown Camino field storage kind: ${String(value)}`); }; const fieldStorageFromOption = (value) => { const object = asObject(value); if (!object) { return { kind: "stored", cache: false, }; } const kind = fieldStorageKindFromOption(object.kind); const resolver = object.resolver === undefined ? undefined : functionRefFromOption(object.resolver); if ((kind === "derived" || kind === "lazy" || kind === "external") && !resolver) { throw new Error(`${kind} field storage requires resolver`); } return { kind, ...(resolver ? { resolver } : {}), cache: object.cache === true, }; }; const fieldOpsFromOption = (value) => { const object = asObject(value); if (!object) { return undefined; } const implementation = enumName(object.implementation); if (implementation !== "SERVICE") { throw new Error(`Unsupported Camino field implementation: ${String(object.implementation)}`); } const service = readString(object, "service"); if (!service) { throw new Error("SERVICE field_ops requires service"); } return { implementation: "service", service, }; }; const extractFileDefaults = (sourceFile) => { const text = fs.readFileSync(sourceFile, "utf8"); const packageMatch = /^\s*package\s+([a-zA-Z0-9_.]+)\s*;/m.exec(text); const namespaceMatch = /^\s*option\s+\(camino\.schema_namespace\)\s*=\s*"([^"]+)"\s*;/m.exec(text); const versionMatch = /^\s*option\s+\(camino\.schema_version\)\s*=\s*"([^"]+)"\s*;/m.exec(text); return { packageName: packageMatch?.[1] ?? "", schemaNamespace: namespaceMatch?.[1] ?? packageMatch?.[1] ?? "", schemaVersion: versionMatch?.[1] ?? "1", }; }; const protoSymbolFromReflection = (reflection, fallbackVersion) => { if (!reflection?.fullName) { return undefined; } const parts = reflection.fullName.replace(/^\./, "").split("."); const name = parts.pop(); if (!name) { return undefined; } return { namespace: parts.join("."), name, version: fallbackVersion, }; }; const typeRefForField = (field, schemaVersion) => { const symbol = protoSymbolFromReflection(field.resolvedType, schemaVersion); const enumValues = field.resolvedType instanceof protobuf.Enum ? Object.entries(field.resolvedType.values) .filter(([_name, value]) => value !== 0) .map(([name]) => name) : undefined; return { protoType: field.type, ...(symbol ? { symbol } : {}), ...(enumValues ? { enumValues } : {}), }; }; const typeRefForType = (type, fallbackType, schemaVersion) => { const symbol = protoSymbolFromReflection(type ?? null, schemaVersion); return { protoType: type?.fullName?.replace(/^\./, "") ?? fallbackType, ...(symbol ? { symbol } : {}), }; }; const fieldSchemaFromField = (field, schemaVersion) => { const storage = fieldStorageFromOption(getReflectionOption(field, FIELD_STORAGE_OPTION)); const ops = fieldOpsFromOption(getReflectionOption(field, FIELD_OPS_OPTION)); if (ops && storage.kind !== "stored" && storage.kind !== "static_final") { throw new Error(`Field ${field.fullName} cannot declare both field_ops and package-resolved storage`); } if (storage.kind === "static_final" && !ops) { throw new Error(`Field ${field.fullName} with static_final storage requires field_ops`); } const interfaceContract = interfaceFieldContractFromOption(getReflectionOption(field, INTERFACE_FIELD_OPTION), schemaVersion); return { name: field.name, tag: field.id, type: typeRefForField(field, schemaVersion), conflict: conflictFromOption(getReflectionOption(field, CONFLICT_OPTION)), repeated: field.repeated, optional: field.optional, storage, ...(ops ? { ops } : {}), ...(interfaceContract ? { interfaceContract } : {}), isDisplayLabel: getReflectionOption(field, DISPLAY_LABEL_OPTION) === true, }; }; const isManyCardinality = (cardinality) => cardinality === "many" || cardinality === "many_unique" || cardinality === "many_ordered" || cardinality === "many_unique_ordered"; const materializationFromOption = (value, fallbackVersion) => { const object = asObject(value); if (!object) { return undefined; } const classRef = symbolRefFromOption(object.class, { namespace: "", name: "", version: fallbackVersion, }); if (!classRef.name) { throw new Error("Edge materialization requires class"); } return { class: classRef, connectProjection: readString(object, "connect_projection") ?? "", }; }; const endpointFromOption = (params) => { const object = asObject(params.option); const classRef = asObject(object?.class) ? symbolRefFromOption(object?.class, { namespace: "", name: "", version: params.fallbackVersion, }) : params.fallbackClass; const interfaceRef = asObject(object?.interface) ? symbolRefFromOption(object?.interface, { namespace: "", name: "", version: params.fallbackVersion, }) : undefined; const projection = readString(object, "projection") ?? params.fallbackProjection; const cardinality = object?.cardinality === undefined ? params.fallbackCardinality : cardinalityFromOption(object.cardinality); const materialization = materializationFromOption(object?.materialize, params.fallbackVersion); if (!classRef?.name && !interfaceRef?.name) { throw new Error(`Edge endpoint ${projection} requires class or interface`); } return { ...(classRef?.name ? { class: classRef } : {}), ...(interfaceRef?.name ? { interface: interfaceRef } : {}), projection, cardinality, indexed: readBoolean(object, "indexed") ?? false, ...(materialization ? { materialization } : {}), }; }; const symbolKey = (ref) => ref ? [ref.namespace, ref.name, ref.version, ref.hash ?? ""].join(":") : ""; const endpointRoleKey = (endpoint) => [ symbolKey(endpoint.class), symbolKey(endpoint.interface), endpoint.projection, endpoint.cardinality, ].join("|"); const normalizeEdgeEndpoints = (params) => { if (params.directionality === "directed") { return { fromEndpoint: params.fromEndpoint, toEndpoint: params.toEndpoint, }; } return endpointRoleKey(params.fromEndpoint) <= endpointRoleKey(params.toEndpoint) ? { fromEndpoint: params.fromEndpoint, toEndpoint: params.toEndpoint, } : { fromEndpoint: params.toEndpoint, toEndpoint: params.fromEndpoint, }; }; const symbolArrayFromOption = (value, defaults) => asArray(value) .map((item) => symbolRefFromOption(item, { namespace: defaults.schemaNamespace, name: "", version: defaults.schemaVersion, })) .filter((symbol) => Boolean(symbol.name)); const isStringType = (type) => type.protoType === "string" || type.protoType === "google.protobuf.StringValue"; const validateDisplayLabelFields = (type, fields, operationServices) => { const displayLabelFields = fields.filter((field) => field.isDisplayLabel); if (displayLabelFields.length > 1) { throw new Error(`Class ${type.fullName} declares multiple Camino display label fields`); } const displayLabelField = displayLabelFields[0]; if (!displayLabelField) { return; } if (!displayLabelField.ops) { if (!isStringType(displayLabelField.type)) { throw new Error(`Display label field ${type.fullName}.${displayLabelField.name} must have string type`); } return; } const serviceName = displayLabelField.ops.service.replace(/^\./, ""); const getOperation = operationServices .find((service) => service.fullName === serviceName) ?.operations.find((operation) => operation.name === "Get"); if (!getOperation || !isStringType(getOperation.outputType)) { throw new Error(`Display label field ${type.fullName}.${displayLabelField.name} must resolve to string`); } }; const typeBindingFromOption = (value, schemaVersion) => { const object = asObject(value); if (!object) { return undefined; } const name = readString(object, "name"); const type = typeRefFromOption(object.type, schemaVersion); if (!name || !type) { throw new Error("Interface type_binding requires name and type"); } return { name, type }; }; const interfaceImplementationFromOption = (value, defaults) => { const object = asObject(value); if (!object) { throw new Error("Expected implements option object"); } const interfaceId = symbolRefFromOption(object.interface, { namespace: defaults.schemaNamespace, name: "", version: defaults.schemaVersion, }); if (!interfaceId.name) { throw new Error("implements option requires interface name"); } return { interface: interfaceId, typeBindings: asArray(object.type_binding) .map((binding) => typeBindingFromOption(binding, defaults.schemaVersion)) .filter((binding) => Boolean(binding)), }; }; const serviceName = (service) => service.fullName.replace(/^\./, ""); const operationFromMethod = (method, schemaVersion) => { const fn = functionRefFromOption(getReflectionOption(method, IMPL_OPTION)); const methodWithTypes = method; return { name: method.name, inputType: typeRefForType(methodWithTypes.resolvedRequestType, method.requestType, schemaVersion), outputType: typeRefForType(methodWithTypes.resolvedResponseType, method.responseType, schemaVersion), function: fn, }; }; const operationServiceFromService = (service, schemaVersion) => ({ name: service.name, fullName: serviceName(service), operations: service.methodsArray.map((method) => operationFromMethod(method, schemaVersion)), }); const classSchemaFromType = (type, sourceFile, defaults, operationServices) => { const classOption = asObject(getReflectionOption(type, CLASS_OPTION)); if (!classOption) { return undefined; } const fallbackClassRef = { namespace: defaults.schemaNamespace, name: type.name, version: defaults.schemaVersion, }; const classId = symbolRefFromOption(classOption.id, fallbackClassRef); const classVersion = readNumber(classOption, "version") ?? Number(classId.version); if (!Number.isInteger(classVersion) || classVersion <= 0) { throw new Error(`Class ${type.fullName} has invalid Camino class version`); } const fields = type.fieldsArray.map((field) => fieldSchemaFromField(field, defaults.schemaVersion)); validateDisplayLabelFields(type, fields, operationServices); const edges = type.fieldsArray.flatMap((field) => { const edgeOption = asObject(getReflectionOption(field, EDGE_OPTION)); if (!edgeOption) { return []; } const legacyTargetClass = symbolRefFromOption(edgeOption.target, { namespace: defaults.schemaNamespace, name: "", version: defaults.schemaVersion, }); const cardinality = cardinalityFromOption(edgeOption.cardinality); const declaredFromEndpoint = endpointFromOption({ option: edgeOption.this_endpoint, fallbackClass: classId, fallbackProjection: field.name, fallbackCardinality: cardinality, fallbackVersion: defaults.schemaVersion, }); const declaredToEndpoint = endpointFromOption({ option: edgeOption.other_endpoint, fallbackClass: legacyTargetClass.name ? legacyTargetClass : undefined, fallbackProjection: readString(edgeOption, "inverse") ?? "", fallbackCardinality: "many", fallbackVersion: defaults.schemaVersion, }); if (!declaredToEndpoint.projection) { throw new Error(`Edge field ${type.fullName}.${field.name} requires other_endpoint.projection or inverse`); } const sourceType = typeRefForField(field, defaults.schemaVersion); if (sourceType.symbol?.namespace !== "camino" || sourceType.symbol.name !== "Ref") { throw new Error(`Edge field ${type.fullName}.${field.name} must use camino.Ref`); } const isMany = isManyCardinality(declaredFromEndpoint.cardinality); if (isMany && !field.repeated) { throw new Error(`Edge field ${type.fullName}.${field.name} must be repeated for ${declaredFromEndpoint.cardinality}`); } if (!isMany && field.repeated) { throw new Error(`Edge field ${type.fullName}.${field.name} must not be repeated for ${declaredFromEndpoint.cardinality}`); } const directionality = edgeOption.undirected === true ? "undirected" : "directed"; const { fromEndpoint, toEndpoint } = normalizeEdgeEndpoints({ directionality, fromEndpoint: declaredFromEndpoint, toEndpoint: declaredToEndpoint, }); const edgeId = symbolRefFromOption(edgeOption.id, { namespace: defaults.schemaNamespace, name: `${type.name}.${field.name}`, version: defaults.schemaVersion, }); const fallbackToClass = toEndpoint.class ?? { namespace: "", name: "", version: "", }; return [ { id: edgeId, directionality, sourceField: fromEndpoint.projection, fromClass: fromEndpoint.class ?? classId, toClass: fallbackToClass, cardinality: fromEndpoint.cardinality, ...(toEndpoint.projection ? { inverse: toEndpoint.projection } : {}), fields: [], fromEndpoint, toEndpoint, declaringClass: classId, tags: symbolArrayFromOption(edgeOption.tag, defaults), implements: symbolArrayFromOption(edgeOption.implements, defaults), }, ]; }); const methods = getReflectionOptions(type, METHOD_OPTION) .map(asObject) .filter((option) => Boolean(option)) .map((option) => { const name = readString(option, "name"); if (!name) { throw new Error(`Method option on ${type.fullName} requires name`); } return { name, function: functionRefFromOption(option.function), }; }); const migrations = getReflectionOptions(type, MIGRATION_OPTION) .map(asObject) .filter((option) => Boolean(option)) .map((option) => { const fromVersion = readNumber(option, "from_version"); const toVersion = readNumber(option, "to_version"); if (!fromVersion || !toVersion) { throw new Error(`Migration option on ${type.fullName} requires versions`); } return { fromVersion, toVersion, function: functionRefFromOption(option.function), }; }); const constructorOption = asObject(getReflectionOption(type, CONSTRUCTOR_OPTION)); const constructorSpec = constructorOption ? { function: functionRefFromOption(constructorOption.function) } : undefined; return { id: classId, version: classVersion, fields, edges, methods, migrations, operationServices, implements: getReflectionOptions(type, IMPLEMENTS_OPTION).map((option) => interfaceImplementationFromOption(option, defaults)), ...(constructorSpec ? { constructorSpec } : {}), sourceFile, protoMessage: type.fullName, }; }; const interfaceSchemaFromType = (type, sourceFile, defaults) => { const interfaceOption = asObject(getReflectionOption(type, INTERFACE_OPTION)); if (!interfaceOption) { return undefined; } const classOption = asObject(getReflectionOption(type, CLASS_OPTION)); if (classOption) { throw new Error(`${type.fullName} cannot be both a Camino class and interface`); } const fallbackInterfaceRef = { namespace: defaults.schemaNamespace, name: type.name, version: defaults.schemaVersion, }; const interfaceId = symbolRefFromOption(interfaceOption.id, fallbackInterfaceRef); const interfaceVersion = readNumber(interfaceOption, "version") ?? Number(interfaceId.version); if (!Number.isInteger(interfaceVersion) || interfaceVersion <= 0) { throw new Error(`Interface ${type.fullName} has invalid Camino interface version`); } return { id: interfaceId, version: interfaceVersion, fields: type.fieldsArray.map((field) => fieldSchemaFromField(field, defaults.schemaVersion)), sourceFile, protoMessage: type.fullName, typeParameters: asArray(interfaceOption.type_parameter) .map((entry) => String(entry).trim()) .filter(Boolean), }; }; const walkTypes = (namespace, visit) => { for (const nested of namespace.nestedArray) { if (nested instanceof protobuf.Type) { visit(nested); walkTypes(nested, visit); } else if (nested instanceof protobuf.Namespace) { walkTypes(nested, visit); } } }; const walkServices = (namespace, visit) => { for (const nested of namespace.nestedArray) { if (nested instanceof protobuf.Service) { visit(nested); } else if (nested instanceof protobuf.Namespace) { walkServices(nested, visit); } } }; export const loadCaminoSchemaRoot = async (sourceFile) => { const absoluteSourceFile = path.resolve(sourceFile); const root = new protobuf.Root(); const envIncludeDirs = [ process.env.QUIXOS_PROTO_PATH, process.env.CAMINO_PROTO_PATH, ] .filter((value) => Boolean(value)) .join(path.delimiter) .split(path.delimiter) .map((entry) => entry.trim()) .filter(Boolean) .map((entry) => path.resolve(entry)); const includeDirs = [ path.dirname(absoluteSourceFile), path.resolve("../../quixos-protocol/proto"), path.resolve("quixos-protocol/proto"), path.resolve("proto"), ...envIncludeDirs, ]; root.resolvePath = (origin, target) => { if (path.isAbsolute(target) && fs.existsSync(target)) { return target; } const originDir = origin ? path.dirname(origin) : undefined; const candidates = [ ...(originDir ? [path.resolve(originDir, target)] : []), ...includeDirs.map((dir) => path.resolve(dir, target)), ]; const found = candidates.find((candidate) => fs.existsSync(candidate)); return found ?? target; }; await root.load(absoluteSourceFile, { keepCase: true }); root.resolveAll(); return { absoluteSourceFile, root }; }; export const compileCaminoSchema = async (sourceFile) => { const { absoluteSourceFile, root } = await loadCaminoSchemaRoot(sourceFile); const defaults = extractFileDefaults(absoluteSourceFile); const operationServices = []; walkServices(root, (service) => { operationServices.push(operationServiceFromService(service, defaults.schemaVersion)); }); const classes = []; const interfaces = []; walkTypes(root, (type) => { const interfaceSchema = interfaceSchemaFromType(type, absoluteSourceFile, defaults); if (interfaceSchema) { interfaces.push(interfaceSchema); } const classSchema = classSchemaFromType(type, absoluteSourceFile, defaults, operationServices); if (classSchema) { classes.push(classSchema); } }); return { sourceFile: absoluteSourceFile, classes, interfaces, }; };