diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 93cecc4..5b4694f 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/flake.lock b/flake.lock index 0c6f604..f465e25 100644 --- a/flake.lock +++ b/flake.lock @@ -74,17 +74,17 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1783905858, - "narHash": "sha256-R8q7hiklpUmKUDs6DCglEsI21CYnMxydZHv9+xq7hj4=", + "lastModified": 1783954303, + "narHash": "sha256-xywk5pWpvOD2rWmNaZ3arYgkx3Km/qsos4g1uB2pXVA=", "ref": "refs/heads/exported", - "rev": "c5141a24c4e82672d66d4b45ac11c45d7d88e1f7", - "revCount": 10, + "rev": "ff07b0d7933ad01c0b52bae2dcab1445e8e91eeb", + "revCount": 14, "type": "git", "url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" }, "original": { "ref": "refs/heads/exported", - "rev": "c5141a24c4e82672d66d4b45ac11c45d7d88e1f7", + "rev": "ff07b0d7933ad01c0b52bae2dcab1445e8e91eeb", "type": "git", "url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" } diff --git a/flake.nix b/flake.nix index 6d3043c..a89aee2 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; - quixos-protocol.url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git?ref=refs/heads/exported&rev=c5141a24c4e82672d66d4b45ac11c45d7d88e1f7"; + quixos-protocol.url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git?ref=refs/heads/exported&rev=ff07b0d7933ad01c0b52bae2dcab1445e8e91eeb"; quixosNixHelpers = { url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/heads/exported&rev=1f0c39b01501d646fe97dfc6e5777ccab0bde2f1"; flake = false; diff --git a/package.json b/package.json index 1cc9c5e..eaa6c54 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "private": true, "packageManager": "yarn@4.14.1", "type": "module", + "bin": { + "camino-codegen-ts-runtime": "dist/src/codegen-ts-runtime.js" + }, "exports": { ".": { "types": "./src/index.ts", @@ -20,7 +23,8 @@ "@bufbuild/protobuf": "^2.12.1", "@connectrpc/connect": "^2.1.2", "@connectrpc/connect-node": "^2.1.2", - "@quixos/camino-datatypes": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a" + "@quixos/camino-datatypes": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a", + "protobufjs": "^7.5.4" }, "devDependencies": { "@bufbuild/protoc-gen-es": "^2.12.1", diff --git a/src/codegen-ts-runtime.ts b/src/codegen-ts-runtime.ts new file mode 100644 index 0000000..d14508f --- /dev/null +++ b/src/codegen-ts-runtime.ts @@ -0,0 +1,414 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { compileCaminoSchema } from "./schema-compiler.js"; +import type { + ClassSchema, + FieldSchema, + FunctionRef, + InterfaceImplementation, + TypeRef, +} from "./schema-ir.js"; + +const usage = (): never => { + console.error( + "Usage: camino-codegen-ts-runtime [implementation-import]", + ); + process.exit(1); +}; + +const required = (value: string | undefined) => value ?? usage(); + +const stringLiteral = (value: string) => JSON.stringify(value); + +const upperFirst = (value: string) => + value.length === 0 ? value : `${value[0]?.toUpperCase()}${value.slice(1)}`; + +const lowerFirst = (value: string) => + value.length === 0 ? value : `${value[0]?.toLowerCase()}${value.slice(1)}`; + +const camel = (value: string) => + value + .split(/[^a-zA-Z0-9]+/) + .filter(Boolean) + .map((part, index) => + index === 0 ? lowerFirst(part) : upperFirst(lowerFirst(part)), + ) + .join(""); + +const isIdentifier = (value: string) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value); + +const classId = (schema: ClassSchema) => + `${schema.id.namespace}:${schema.id.name}:${schema.id.version}:${ + schema.id.hash ?? "" + }`; + +const symbolId = (value: { namespace: string; name: string; version: string; hash?: string }) => + `${value.namespace}:${value.name}:${value.version}:${value.hash ?? ""}`; + +const tsTypeForTypeRef = (type: TypeRef): string => { + if (type.enumValues && type.enumValues.length > 0) { + return type.enumValues.map(stringLiteral).join(" | "); + } + switch (type.protoType.replace(/^\./, "")) { + case "string": + case "google.protobuf.StringValue": + return "string"; + case "bool": + case "google.protobuf.BoolValue": + return "boolean"; + case "double": + case "float": + case "google.protobuf.DoubleValue": + case "google.protobuf.FloatValue": + case "int32": + case "sint32": + case "sfixed32": + case "uint32": + case "fixed32": + case "int64": + case "sint64": + case "sfixed64": + case "uint64": + case "fixed64": + case "google.protobuf.Int32Value": + case "google.protobuf.UInt32Value": + case "google.protobuf.Int64Value": + case "google.protobuf.UInt64Value": + return "number"; + case "bytes": + case "google.protobuf.BytesValue": + return "Uint8Array"; + default: + return "unknown"; + } +}; + +const tsTypeForField = (field: FieldSchema): string => tsTypeForTypeRef(field.type); + +const tsValueTypeForField = (field: FieldSchema) => + field.repeated ? `${tsTypeForField(field)}[]` : tsTypeForField(field); + +const tsFieldApiType = (field: FieldSchema) => + field.conflict === "crdt" + ? `CrdtField<${tsValueTypeForField(field)}>` + : `Field<${tsValueTypeForField(field)}>`; + +const functionExportName = (fn: FunctionRef, fallback: string) => { + if (isIdentifier(fn.symbol)) { + return fn.symbol; + } + const fallbackName = camel(fallback); + if (isIdentifier(fallbackName)) { + return fallbackName; + } + throw new Error(`Cannot generate TS export name for function ${fn.symbol}`); +}; + +const generatedWarning = `// @generated by camino-codegen-ts-runtime. Do not edit.\n`; + +type FunctionSpec = { + exportName: string; + symbol: string; + operation?: string; + packageNamespace: string; + packageName: string; + field?: { + name: string; + type: string; + storage: string; + conflict: string; + }; +}; + +const fieldTypeId = (field: FieldSchema) => + field.type.symbol ? symbolId(field.type.symbol) : field.type.protoType; + +const crdtStorageTypeId = (field: FieldSchema) => + field.type.protoType || + (field.type.symbol + ? `${field.type.symbol.namespace}.${field.type.symbol.name}` + : fieldTypeId(field)); + +const fieldStorageId = (field: FieldSchema) => + field.ops && field.storage.kind === "stored" ? "derived" : field.storage.kind; + +const interfaceTypeName = (implementation: InterfaceImplementation) => + `${implementation.interface.name}Ref`; + +const renderInterfaceRefTypes = (schema: ClassSchema) => + schema.implements + .map((implementation) => { + const typeParams = implementation.typeBindings.map((binding) => binding.name); + const generic = + typeParams.length > 0 ? `<${typeParams.join(", ")}>` : ""; + const bindings = + typeParams.length > 0 + ? typeParams + .map((param) => ` readonly ${param}: ${param};`) + .join("\n") + : " readonly value: unknown;"; + return `export type ${interfaceTypeName(implementation)}${generic} = ObjectRef & { + readonly $caminoInterfaces: { + readonly ${stringLiteral(symbolId(implementation.interface))}: { +${bindings} + }; + }; +};`; + }) + .join("\n\n"); + +const interfaceMixins = (schema: ClassSchema) => + schema.implements.map((implementation) => { + const typeArgs = implementation.typeBindings.map((binding) => + tsTypeForTypeRef(binding.type), + ); + return `${interfaceTypeName(implementation)}${ + typeArgs.length > 0 ? `<${typeArgs.join(", ")}>` : "" + }`; + }); + +const operationFunctionSpecs = (schema: ClassSchema): FunctionSpec[] => + schema.fields.flatMap((field) => { + const serviceName = field.ops?.service.replace(/^\./, ""); + if (!serviceName) { + return []; + } + const service = schema.operationServices.find( + (candidate) => candidate.fullName === serviceName, + ); + if (!service) { + throw new Error( + `${schema.id.name}.${field.name} points to missing operation service ${field.ops?.service}`, + ); + } + return service.operations.map((operation) => ({ + exportName: functionExportName( + operation.function, + `${field.name}_${operation.name}`, + ), + symbol: operation.function.symbol, + ...(operation.function.operation + ? { operation: operation.function.operation } + : {}), + packageNamespace: operation.function.packageNamespace, + packageName: operation.function.packageName, + field: { + name: field.name, + type: fieldTypeId(field), + storage: fieldStorageId(field), + conflict: field.conflict, + }, + })); + }); + +const renderRuntime = (schema: ClassSchema) => { + const className = schema.id.name; + const objectTypeName = `${className}Object`; + const refTypeName = `${className}Ref`; + const contextTypeName = `${className}Context`; + const implementationTypeName = `${className}Implementation`; + const serveName = `serve${className}Runtime`; + const createObjectName = `create${className}Object`; + const interfaceRefTypes = renderInterfaceRefTypes(schema); + const refType = + [ `ObjectRef<${stringLiteral(classId(schema))}>`, ...interfaceMixins(schema)] + .join(" & "); + const edgeFields = new Set(schema.edges.map((edge) => edge.sourceField)); + const writableFields = schema.fields.filter( + (field) => + field.storage.kind === "stored" && + !field.ops && + !edgeFields.has(field.name), + ); + const functionSpecs = [ + ...(schema.constructorSpec + ? [ + { + exportName: functionExportName( + schema.constructorSpec.function, + "constructor", + ), + symbol: schema.constructorSpec.function.symbol, + ...(schema.constructorSpec.function.operation + ? { operation: schema.constructorSpec.function.operation } + : {}), + packageNamespace: schema.constructorSpec.function.packageNamespace, + packageName: schema.constructorSpec.function.packageName, + }, + ] + : []), + ...schema.methods.map((method) => ({ + exportName: functionExportName(method.function, method.name), + symbol: method.function.symbol, + ...(method.function.operation ? { operation: method.function.operation } : {}), + packageNamespace: method.function.packageNamespace, + packageName: method.function.packageName, + })), + ...schema.fields.flatMap((field) => + field.storage.resolver + ? [ + { + exportName: functionExportName(field.storage.resolver, field.name), + symbol: field.storage.resolver.symbol, + ...(field.storage.resolver.operation + ? { operation: field.storage.resolver.operation } + : {}), + packageNamespace: field.storage.resolver.packageNamespace, + packageName: field.storage.resolver.packageName, + }, + ] + : [], + ), + ...operationFunctionSpecs(schema), + ]; + const firstFunction = functionSpecs[0]; + if (!firstFunction) { + throw new Error(`${schema.id.name} has no runtime functions to generate`); + } + const mixedPackage = functionSpecs.find( + (spec) => + spec.packageNamespace !== firstFunction.packageNamespace || + spec.packageName !== firstFunction.packageName, + ); + if (mixedPackage) { + throw new Error( + `${schema.id.name} points to multiple packages; v0 TS runtime generation expects one package`, + ); + } + + return `${generatedWarning}import { + type CaminoClient, + createCrdtField, + createField, + type CrdtField, + type Field, + type FieldOperation, + type InvokeRequest, + objectRef, + type ObjectRef, + type RuntimeHandler, + serveQuixosPackageRuntime, +} from "@quixos/camino-package-runtime"; + +${interfaceRefTypes ? `${interfaceRefTypes}\n\n` : ""}export type ${refTypeName} = ${refType}; + +export type ${objectTypeName} = { + id: ${refTypeName}; +${writableFields + .map( + (field) => + ` ${camel(field.name)}: ${tsFieldApiType(field)};`, + ) + .join("\n")} +}; + +export type ${contextTypeName} = { + object: ${objectTypeName}; + camino: CaminoClient; + request: InvokeRequest; +}; + +export type ${implementationTypeName} = { +${functionSpecs + .map((spec) => + spec.operation + ? ` ${spec.exportName}: FieldOperation<${contextTypeName}>;` + : ` ${spec.exportName}: RuntimeHandler<${contextTypeName}>;`, + ) + .filter((line, index, lines) => lines.indexOf(line) === index) + .join("\n")} +}; + +const packageNamespace = ${stringLiteral(firstFunction.packageNamespace)}; +const packageName = ${stringLiteral(firstFunction.packageName)}; +const runtimeProtocolVersion = ${stringLiteral("camino-orch-v0")}; + +const functions = [ +${functionSpecs + .map( + (spec) => + ` { exportName: ${stringLiteral(spec.exportName)}, symbol: ${stringLiteral(spec.symbol)}${ + spec.operation ? `, operation: ${stringLiteral(spec.operation)}` : "" + }${ + spec.field + ? `, field: { name: ${stringLiteral(spec.field.name)}, type: ${stringLiteral(spec.field.type)}, storage: ${stringLiteral(spec.field.storage)}, conflict: ${stringLiteral(spec.field.conflict)} }` + : "" + } },`, + ) + .join("\n")} +] as const; + +export const ${createObjectName} = ( + camino: CaminoClient, + objectId: string, +): ${objectTypeName} => { + const id = objectRef<${stringLiteral(classId(schema))}>(objectId) as ${refTypeName}; + return { + id, +${writableFields + .map( + (fieldSchema) => + fieldSchema.conflict === "crdt" + ? ` ${camel(fieldSchema.name)}: createCrdtField(camino, id, ${stringLiteral(fieldSchema.name)}, ${stringLiteral(crdtStorageTypeId(fieldSchema))}),` + : ` ${camel(fieldSchema.name)}: createField(camino, id, ${stringLiteral(fieldSchema.name)}),`, + ) + .join("\n")} + }; +}; + +export const create${className}Context = ( + camino: CaminoClient, + request: InvokeRequest, +): ${contextTypeName} => ({ + object: ${createObjectName}(camino, request.objectId), + camino, + request, +}); + +export const ${serveName} = (implementation: ${implementationTypeName}) => { + serveQuixosPackageRuntime({ + packageNamespace, + packageName, + runtimeProtocolVersion, + functions, + implementation, + createContext: create${className}Context, + }); +}; +`; +}; + +const renderServer = (schema: ClassSchema, implementationImport: string) => { + const className = schema.id.name; + return `${generatedWarning}import * as implementation from ${stringLiteral(implementationImport)}; +import { serve${className}Runtime } from "./${schema.id.namespace}.${className}.runtime.js"; + +serve${className}Runtime(implementation); +`; +}; + +const main = async () => { + const sourceFile = required(process.argv[2]); + const outDir = required(process.argv[3]); + const implementationImport = process.argv[4] ?? "../task.impl.js"; + const compiled = await compileCaminoSchema(sourceFile); + if (compiled.classes.length !== 1) { + throw new Error( + `Expected exactly one Camino class in ${sourceFile}, found ${compiled.classes.length}`, + ); + } + const schema = compiled.classes[0]; + if (!schema) { + throw new Error(`No Camino class found in ${sourceFile}`); + } + fs.mkdirSync(outDir, { recursive: true }); + const baseName = `${schema.id.namespace}.${schema.id.name}`; + fs.writeFileSync(path.join(outDir, `${baseName}.runtime.ts`), renderRuntime(schema)); + fs.writeFileSync( + path.join(outDir, `${baseName}.server.ts`), + renderServer(schema, implementationImport), + ); +}; + +await main(); diff --git a/src/schema-compiler.ts b/src/schema-compiler.ts new file mode 100644 index 0000000..3b9354a --- /dev/null +++ b/src/schema-compiler.ts @@ -0,0 +1,809 @@ +import fs from "node:fs"; +import path from "node:path"; +import protobuf from "protobufjs"; +import type { + Cardinality, + ClassSchema, + ConflictStrategy, + FieldSchema, + FieldStorage, + FieldStorageKind, + FunctionRef, + InterfaceFieldContract, + InterfaceImplementation, + InterfaceSchema, + MethodSchema, + OperationSchema, + OperationServiceSchema, + MigrationSpec, + SchemaCompileResult, + SymbolRef, + TypeRef, +} from "./schema-ir.js"; + +type OptionBag = Record; + +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: unknown) => + String(value) + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/-/g, "_") + .toUpperCase(); + +const lowerEnumName = (value: unknown) => enumName(value).toLowerCase(); + +const getOption = ( + options: OptionBag | undefined, + name: string, +): unknown => { + 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: protobuf.ReflectionObject, +): OptionBag[] => { + const parsedOptions = (reflection as unknown as { parsedOptions?: unknown }) + .parsedOptions; + return Array.isArray(parsedOptions) + ? parsedOptions.filter( + (option): option is OptionBag => + Boolean(option) && typeof option === "object" && !Array.isArray(option), + ) + : []; +}; + +const getReflectionOptions = ( + reflection: protobuf.ReflectionObject, + name: string, +): unknown[] => { + 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: protobuf.ReflectionObject, + name: string, +): unknown => getReflectionOptions(reflection, name)[0]; + +const asObject = (value: unknown): OptionBag | undefined => + value && typeof value === "object" && !Array.isArray(value) + ? (value as OptionBag) + : undefined; + +const asArray = (value: unknown): unknown[] => + Array.isArray(value) ? value : value === undefined ? [] : [value]; + +const readString = ( + object: OptionBag | undefined, + key: string, +): string | undefined => { + const value = object?.[key]; + return typeof value === "string" ? value : undefined; +}; + +const readNumber = ( + object: OptionBag | undefined, + key: string, +): number | undefined => { + const value = object?.[key]; + return typeof value === "number" && Number.isFinite(value) + ? value + : undefined; +}; + +const readBoolean = ( + object: OptionBag | undefined, + key: string, +): boolean | undefined => { + const value = object?.[key]; + return typeof value === "boolean" ? value : undefined; +}; + +const symbolRefFromOption = ( + value: unknown, + fallback: SymbolRef, +): SymbolRef => { + 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: unknown, + fallbackVersion: string, +): TypeRef | undefined => { + 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: unknown, + schemaVersion: string, +): InterfaceFieldContract | undefined => { + const object = asObject(value); + if (!object) { + return undefined; + } + const type = typeRefFromOption(object.type, schemaVersion); + 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 } : {}), + }; +}; + +const functionRefFromOption = (value: unknown): FunctionRef => { + 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: unknown): ConflictStrategy => { + 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: unknown): Cardinality => { + 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" + ) { + return normalized; + } + throw new Error(`Unknown Camino cardinality: ${String(value)}`); +}; + +const fieldStorageKindFromOption = (value: unknown): FieldStorageKind => { + 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: unknown): FieldStorage => { + 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: unknown) => { + 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" as const, + service, + }; +}; + +const extractFileDefaults = (sourceFile: string) => { + 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: protobuf.ReflectionObject | null, + fallbackVersion: string, +): SymbolRef | undefined => { + 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: protobuf.Field, + schemaVersion: string, +): TypeRef => { + 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: protobuf.Type | undefined, + fallbackType: string, + schemaVersion: string, +): TypeRef => { + const symbol = protoSymbolFromReflection(type ?? null, schemaVersion); + return { + protoType: type?.fullName?.replace(/^\./, "") ?? fallbackType, + ...(symbol ? { symbol } : {}), + }; +}; + +const fieldSchemaFromField = ( + field: protobuf.Field, + schemaVersion: string, +): FieldSchema => { + 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 isStringType = (type: TypeRef) => + type.protoType === "string" || type.protoType === "google.protobuf.StringValue"; + +const validateDisplayLabelFields = ( + type: protobuf.Type, + fields: FieldSchema[], + operationServices: OperationServiceSchema[], +) => { + 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: unknown, + schemaVersion: string, +) => { + 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: unknown, + defaults: ReturnType, +): InterfaceImplementation => { + 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): binding is NonNullable => + Boolean(binding), + ), + }; +}; + +const serviceName = (service: protobuf.Service) => + service.fullName.replace(/^\./, ""); + +const operationFromMethod = ( + method: protobuf.Method, + schemaVersion: string, +): OperationSchema => { + const fn = functionRefFromOption(getReflectionOption(method, IMPL_OPTION)); + const methodWithTypes = method as protobuf.Method & { + resolvedRequestType?: protobuf.Type; + resolvedResponseType?: protobuf.Type; + }; + return { + name: method.name, + inputType: typeRefForType( + methodWithTypes.resolvedRequestType, + method.requestType, + schemaVersion, + ), + outputType: typeRefForType( + methodWithTypes.resolvedResponseType, + method.responseType, + schemaVersion, + ), + function: fn, + }; +}; + +const operationServiceFromService = ( + service: protobuf.Service, + schemaVersion: string, +): OperationServiceSchema => ({ + name: service.name, + fullName: serviceName(service), + operations: service.methodsArray.map((method) => + operationFromMethod(method, schemaVersion), + ), +}); + +const classSchemaFromType = ( + type: protobuf.Type, + sourceFile: string, + defaults: ReturnType, + operationServices: OperationServiceSchema[], +): ClassSchema | undefined => { + 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 targetClass = symbolRefFromOption(edgeOption.target, { + namespace: defaults.schemaNamespace, + name: "", + version: defaults.schemaVersion, + }); + if (!targetClass.name) { + throw new Error(`Edge field ${type.fullName}.${field.name} requires target`); + } + const cardinality = cardinalityFromOption(edgeOption.cardinality); + 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 = + cardinality === "many" || + cardinality === "many_unique" || + cardinality === "many_ordered"; + if (isMany && !field.repeated) { + throw new Error(`Edge field ${type.fullName}.${field.name} must be repeated for ${cardinality}`); + } + if (!isMany && field.repeated) { + throw new Error(`Edge field ${type.fullName}.${field.name} must not be repeated for ${cardinality}`); + } + const edgeId = symbolRefFromOption(edgeOption.id, { + namespace: defaults.schemaNamespace, + name: `${type.name}.${field.name}`, + version: defaults.schemaVersion, + }); + return [ + { + id: edgeId, + sourceField: field.name, + fromClass: classId, + toClass: targetClass, + cardinality, + ...(readString(edgeOption, "inverse") + ? { inverse: readString(edgeOption, "inverse") } + : {}), + fields: [], + }, + ]; + }); + + const methods: MethodSchema[] = getReflectionOptions(type, METHOD_OPTION) + .map(asObject) + .filter((option): option is OptionBag => 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: MigrationSpec[] = getReflectionOptions(type, MIGRATION_OPTION) + .map(asObject) + .filter((option): option is OptionBag => 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: protobuf.Type, + sourceFile: string, + defaults: ReturnType, +): InterfaceSchema | undefined => { + 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, + }; +}; + +const walkTypes = ( + namespace: protobuf.NamespaceBase, + visit: (type: protobuf.Type) => void, +) => { + 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: protobuf.NamespaceBase, + visit: (service: protobuf.Service) => void, +) => { + for (const nested of namespace.nestedArray) { + if (nested instanceof protobuf.Service) { + visit(nested); + } else if (nested instanceof protobuf.Namespace) { + walkServices(nested, visit); + } + } +}; + +export const compileCaminoSchema = async ( + sourceFile: string, +): Promise => { + const absoluteSourceFile = path.resolve(sourceFile); + const defaults = extractFileDefaults(absoluteSourceFile); + const root = new protobuf.Root(); + const envIncludeDirs = [ + process.env.QUIXOS_PROTO_PATH, + process.env.CAMINO_PROTO_PATH, + ] + .filter((value): value is string => 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(); + + const operationServices: OperationServiceSchema[] = []; + walkServices(root, (service) => { + operationServices.push( + operationServiceFromService(service, defaults.schemaVersion), + ); + }); + + const classes: ClassSchema[] = []; + const interfaces: InterfaceSchema[] = []; + 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, + }; +}; diff --git a/src/schema-ir.ts b/src/schema-ir.ts new file mode 100644 index 0000000..92fc5b0 --- /dev/null +++ b/src/schema-ir.ts @@ -0,0 +1,148 @@ +export type SymbolRef = { + namespace: string; + name: string; + version: string; + hash?: string; +}; + +export type FunctionRef = { + packageNamespace: string; + packageName: string; + symbol: string; + operation?: string; + versionRef?: string; +}; + +export type ConflictStrategy = + | "replace" + | "preserve_conflicts" + | "crdt"; + +export type Cardinality = + | "optional_one" + | "exactly_one" + | "many" + | "many_unique" + | "many_ordered"; + +export type FieldStorageKind = + | "stored" + | "derived" + | "lazy" + | "external" + | "static_final"; + +export type FieldStorage = { + kind: FieldStorageKind; + resolver?: FunctionRef; + cache: boolean; +}; + +export type FieldOps = { + implementation: "service"; + service: string; +}; + +export type TypeRef = { + protoType: string; + symbol?: SymbolRef; + enumValues?: string[]; +}; + +export type TypeBinding = { + name: string; + type: TypeRef; +}; + +export type InterfaceFieldContract = { + required: boolean; + readable: boolean; + writable: boolean; + watchable: boolean; + typeParam?: string; + type?: TypeRef; +}; + +export type InterfaceImplementation = { + interface: SymbolRef; + typeBindings: TypeBinding[]; +}; + +export type FieldSchema = { + name: string; + tag: number; + type: TypeRef; + conflict: ConflictStrategy; + repeated: boolean; + optional: boolean; + storage: FieldStorage; + ops?: FieldOps; + interfaceContract?: InterfaceFieldContract; + isDisplayLabel: boolean; +}; + +export type EdgeSchema = { + id: SymbolRef; + sourceField: string; + fromClass: SymbolRef; + toClass: SymbolRef; + cardinality: Cardinality; + inverse?: string; + fields: FieldSchema[]; +}; + +export type MethodSchema = { + name: string; + function: FunctionRef; +}; + +export type MigrationSpec = { + fromVersion: number; + toVersion: number; + function: FunctionRef; +}; + +export type ConstructorSpec = { + function: FunctionRef; +}; + +export type OperationSchema = { + name: string; + inputType: TypeRef; + outputType: TypeRef; + function: FunctionRef; +}; + +export type OperationServiceSchema = { + name: string; + fullName: string; + operations: OperationSchema[]; +}; + +export type ClassSchema = { + id: SymbolRef; + version: number; + fields: FieldSchema[]; + edges: EdgeSchema[]; + methods: MethodSchema[]; + migrations: MigrationSpec[]; + operationServices: OperationServiceSchema[]; + implements: InterfaceImplementation[]; + constructorSpec?: ConstructorSpec; + sourceFile: string; + protoMessage: string; +}; + +export type InterfaceSchema = { + id: SymbolRef; + version: number; + fields: FieldSchema[]; + sourceFile: string; + protoMessage: string; +}; + +export type SchemaCompileResult = { + sourceFile: string; + classes: ClassSchema[]; + interfaces: InterfaceSchema[]; +}; diff --git a/yarn-project.nix b/yarn-project.nix index 8e90bd7..5e99fde 100644 --- a/yarn-project.nix +++ b/yarn-project.nix @@ -162,16 +162,29 @@ cacheEntries = { "@bufbuild/protoplugin@npm:2.12.1" = { filename = "@bufbuild-protoplugin-npm-2.12.1-e1aebb80bf-c7b401ab31.zip"; hash = "sha512-x7QBqzHZSsaCXLm75c/3XQXSjuINRnskt6XUKZuaygZmhuo5t6OoRTuLWgGD+qd4HoP8LliAZhgHudMVX3Hr6Q=="; }; "@connectrpc/connect-node@npm:2.1.2" = { filename = "@connectrpc-connect-node-npm-2.1.2-021434d5d5-ba8b0c9384.zip"; hash = "sha512-uosMk4TDgps3eqeoJtwM1Qpgk1J9LscBRgSRKDYzbqlgSitazqQrEEd/i+RWnknd7WbKD8d/X1sCcDXgKeE+qQ=="; }; "@connectrpc/connect@npm:2.1.2" = { filename = "@connectrpc-connect-npm-2.1.2-ab34825de3-4e15000890.zip"; hash = "sha512-ThUACJAjt1Lv1E+pQKWuHh+gDhvrxLoM7ScruTWSDYw7hbd0UOZErUQruMqaJ1sMFxNviWuo7/A2F4Ex2wHWFQ=="; }; +"@protobufjs/aspromise@npm:1.1.2" = { filename = "@protobufjs-aspromise-npm-1.1.2-71d00b938f-a83343a468.zip"; hash = "sha512-qDNDpGj/W17Gv/Nv14imTIOeSKB/+fT4E1ZPWMr0TQEc1lBO0hR780g1vXp90hBwUq91WWHGsJj9iQK09lANDw=="; }; +"@protobufjs/base64@npm:1.1.2" = { filename = "@protobufjs-base64-npm-1.1.2-cd8ca6814a-eec925e681.zip"; hash = "sha512-7skl5oEIGvGQuO4jH5utMQHhiau8GC/yedprUx59vSpW8fMG83qAsb6eAKotJxaQ0I3MXzJvccnu2FRmdcjK9g=="; }; +"@protobufjs/codegen@npm:2.0.5" = { filename = "@protobufjs-codegen-npm-2.0.5-bb74ff329d-1b8a2ae56e.zip"; hash = "sha512-G4oq5W7mClbp0gXNS2ByoVA8UGm467kFcQ+XT/AJig0HAGQcE34KjZje3xRCMVahBqlDNpXL9SV0gQ9VAA/cqw=="; }; +"@protobufjs/eventemitter@npm:1.1.1" = { filename = "@protobufjs-eventemitter-npm-1.1.1-dbe0dfc812-8e06193d46.zip"; hash = "sha512-jgYZPUYpxefAnU+MLduo/E36c58BSfM6HZAVaNNbt7i1J3pOhFK6873QswL9WZzyVdGTJnqpOgpHR+I80HPErA=="; }; +"@protobufjs/fetch@npm:1.1.1" = { filename = "@protobufjs-fetch-npm-1.1.1-b98d5396a8-a497ff5433.zip"; hash = "sha512-pJf/VDOFToV38EJ5g+o5uRE7SagSD5RRUpHXYzJwYdLDAT5g4k6kNtCR2vrgGg9usYZ+OxYWBF2Wox2LPGRu1A=="; }; +"@protobufjs/float@npm:1.0.2" = { filename = "@protobufjs-float-npm-1.0.2-5678f64d08-18f2bdede7.zip"; hash = "sha512-GPK97edv/PAXBwivFcnJ22JZt3HmuExRsG3zSpwznbvuwmfRTOC93SCswUKx2YDZg9MUNDmN9/mOsMlKDreQaQ=="; }; +"@protobufjs/path@npm:1.1.2" = { filename = "@protobufjs-path-npm-1.1.2-641d08de76-cece0a938e.zip"; hash = "sha512-zs4Kk45/Xf0voD+MFPLxz4sNbhOscyb/TJbqMR7/1ft64LunVPv1BTEq8uOFACUMkOaFBrl8AjYKQ3k9iKDYtA=="; }; +"@protobufjs/pool@npm:1.1.0" = { filename = "@protobufjs-pool-npm-1.1.0-47a76f96a1-eda2718b7f.zip"; hash = "sha512-7aJxi38iKsbmrTb3WKku+Q0mUmAmoZ9PF/Zo9F4DBqW9c03vP0j1H4E0rgl4tiYqXFF8CLEVpVF1bRo6rfzwOA=="; }; +"@protobufjs/utf8@npm:1.1.2" = { filename = "@protobufjs-utf8-npm-1.1.2-9c7ca5968f-975c6e2c03.zip"; hash = "sha512-l1xuLAMZvVDBdTHRhrU3qIy00yBePKkpfNhCtjHTQdoO4/8WvoZWzSZgdW3HU7KcimoUwK2O1gLB+R3fIuez8w=="; }; "@quixos/camino-datatypes@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a" = { filename = "@quixos-camino-datatypes-https-faf980267e-4ed01c75c1.zip"; hash = "sha512-TtAcdcG7mReSI/kmbu+U91w39uYetoOiQbq6ex48zaTphDXn1vO1aaw0k8kEDhQgE1LMZrFgHyPzjlUIkyKdrA=="; }; "@types/node@npm:24.13.3" = { filename = "@types-node-npm-24.13.3-b512a0bbeb-a5bc08f49b.zip"; hash = "sha512-pbwI9JuVgdzcqQ4CzXcZejx5mECAdmSULlzVFhpVPU0q6AZPfjKUQQ10jX0Ji1wYSL5/pQwG8pxBk6sWvk5Z6w=="; }; +"@types/node@npm:26.1.1" = { filename = "@types-node-npm-26.1.1-7a7a9f32f4-25ac509319.zip"; hash = "sha512-JaxQkxlXNt0HTVAnrbuoVhe8lR1aQVBuvTuQcwSKy3IzYT84ItX5uUR/nAhByR9jh8Rig5FnZ+PvefI5F0UsRg=="; }; "@typescript/vfs@npm:1.6.4" = { filename = "@typescript-vfs-npm-1.6.4-78935c9d3e-acb9de42f2.zip"; hash = "sha512-rLneQvI/2j9149eQC6EG7zI6L0588OSpTbtFfibTU8pjuzUZPtGjL8hzPzrlkJlhKykLBr1iIWlIYb65v7YvYg=="; }; "debug@npm:4.4.3" = { filename = "debug-npm-4.4.3-0105c6123a-d79136ec6c.zip"; hash = "sha512-15E27GyD7L79D2pVk9pqnJHsTX3cS1TIg9bnHsmsy19noaXpbQCjKBlrW1yG02XpjYo6cIVqrxa057GYXmf1pg=="; }; +"long@npm:5.3.2" = { filename = "long-npm-5.3.2-f80d0f7d39-7130fe1cbc.zip"; hash = "sha512-cTD+HLzi3KBnNLNbcNOAyj9wJxx/iFLJIqfGLIbE418MOSkFZeynEzxiWQjUDhJqxXwCsbGkY2uUV9d+HmC5gQ=="; }; "ms@npm:2.1.3" = { filename = "ms-npm-2.1.3-81ff3cfac1-d924b57e73.zip"; hash = "sha512-2SS1fnMSs7Y60h/Fs9wK9eeNYaH8fPtUV+2vJjJr9ivlMHzIf/toYu8cKzOwIzzbXU8BxMlYzA1mCUi2Wih6SA=="; }; +"protobufjs@npm:7.6.5" = { filename = "protobufjs-npm-7.6.5-ff4a872120-863eaca9c6.zip"; hash = "sha512-hj6sqcb0W/z7h4fFRfU+nGaWUH4yjjmBtGQ8EtNd9/KCYCHBDj/TC+80LBOo6dMGVHusnAhJ7jv1D3cKEvAdxQ=="; }; "typescript@npm:5.4.5" = { filename = "typescript-npm-5.4.5-8568a42232-2954022ada.zip"; hash = "sha512-KVQCKto0D9PWqeK45TT2XVfJLV85iaJjdUp4q6VJ9+ZSmswZIZE1YKS4FsRtzn30pNKfnxGj3A1CE7t20EMlHg=="; }; "typescript@npm:5.9.3" = { filename = "typescript-npm-5.9.3-48715be868-6bd7552ce3.zip"; hash = "sha512-a9dVLOOfl+cR21qgSPb5mVtT8cUvfYZnwavcFwDGinajCPV5zTCc5rU2Rt606aG+fIE6k7qvCijM1TajAnDhxQ=="; }; "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" = { filename = "typescript-patch-6e159bfddb-db2ad2a16c.zip"; hash = "sha512-2yrSoWyoKfUEJ+6x2hVeekXlmO7HsIbYtOi6ROWiNfdY5gbWgcZpkiMNP8O4mVhl5f0LIqLJVIbQsyAPgwcuyQ=="; }; "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" = { filename = "typescript-patch-6fda4d02cf-ad09fdf7a7.zip"; hash = "sha512-rQn996dWgU3OZbxgwWV7QNREUTRoWO6iMOEPLpWiidkYO24y5cEelazAzMIUtPNiidytS/GIawrbhNcR0zakMA=="; }; "undici-types@npm:7.18.2" = { filename = "undici-types-npm-7.18.2-3e6d69d829-85a7918911.zip"; hash = "sha512-haeRiRE6I4lZ16ZHNo5PfFVZw6QE69uPxEiBRc6UJvzYIlKoRKMCeY38Djfm+xeP9IHtA7xMr2NMV1fZ70NSHQ=="; }; +"undici-types@npm:8.3.0" = { filename = "undici-types-npm-8.3.0-d34470de3e-c8aa7e2fbe.zip"; hash = "sha512-yKp+L76/zlGWVNr63A7OWb6IjSzK8YD7RJXah157U20kVjRcOEBpx+bz6cmrdDXwdJV9owbxQjQ+7ob/gEiFWg=="; }; }; in overriddenProject diff --git a/yarn.lock b/yarn.lock index be65bd7..b47d038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -66,6 +66,71 @@ __metadata: languageName: node linkType: hard +"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/aspromise@npm:1.1.2" + checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f + languageName: node + linkType: hard + +"@protobufjs/base64@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/base64@npm:1.1.2" + checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6 + languageName: node + linkType: hard + +"@protobufjs/codegen@npm:^2.0.5": + version: 2.0.5 + resolution: "@protobufjs/codegen@npm:2.0.5" + checksum: 10c0/1b8a2ae56ee60a56e9d205cd4b6072a1503c5069b8ebb905710f974ff0098a0d0700641c137e0a8d98dedf14423156a106a9433695cbf52574810f55000fdcab + languageName: node + linkType: hard + +"@protobufjs/eventemitter@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/eventemitter@npm:1.1.1" + checksum: 10c0/8e06193d4629c5e7c09d4f8c2ddba8fc4dfa739f0149f33a1d901568d35bb7b8b5277a4e8452baf3bdd0b302fd599cf255d193267aa93a0a4747e23cd073c4ac + languageName: node + linkType: hard + +"@protobufjs/fetch@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/fetch@npm:1.1.1" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.1" + checksum: 10c0/a497ff5433854e8577f0427983ea39b9113b49a8120f94515291d763327061d2c3013e60e24ea436d091dafae01a0f6eb1867e3b1616045d96a31d8b3c646ed4 + languageName: node + linkType: hard + +"@protobufjs/float@npm:^1.0.2": + version: 1.0.2 + resolution: "@protobufjs/float@npm:1.0.2" + checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069 + languageName: node + linkType: hard + +"@protobufjs/path@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/path@npm:1.1.2" + checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4 + languageName: node + linkType: hard + +"@protobufjs/pool@npm:^1.1.0": + version: 1.1.0 + resolution: "@protobufjs/pool@npm:1.1.0" + checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038 + languageName: node + linkType: hard + +"@protobufjs/utf8@npm:^1.1.1": + version: 1.1.2 + resolution: "@protobufjs/utf8@npm:1.1.2" + checksum: 10c0/975c6e2c0319bd50c17531d186b537a88cb4d3205e3ca9297cd842b631d341da0ee3ff16be8656cd2660756dc753b29c8a6a14c0ad8ed602c1f91ddf22e7b3f3 + languageName: node + linkType: hard + "@quixos/camino-datatypes@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a": version: 0.1.0 resolution: "@quixos/camino-datatypes@https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a" @@ -86,10 +151,22 @@ __metadata: "@connectrpc/connect-node": "npm:^2.1.2" "@quixos/camino-datatypes": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git#commit=e061e290709c672d2dc675e5c871733de195211a" "@types/node": "npm:^24" + protobufjs: "npm:^7.5.4" typescript: "npm:^5.9.3" + bin: + camino-codegen-ts-runtime: dist/src/codegen-ts-runtime.js languageName: unknown linkType: soft +"@types/node@npm:>=13.7.0": + version: 26.1.1 + resolution: "@types/node@npm:26.1.1" + dependencies: + undici-types: "npm:~8.3.0" + checksum: 10c0/25ac5093195736dd074d5027adbba85617bc951d5a41506ebd3b9073048acb7233613f3822d5f9b9447f9c0841c91f6387c46283916767e3ef79f23917452c46 + languageName: node + linkType: hard + "@types/node@npm:^24": version: 24.13.3 resolution: "@types/node@npm:24.13.3" @@ -122,6 +199,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^5.3.2": + version: 5.3.2 + resolution: "long@npm:5.3.2" + checksum: 10c0/7130fe1cbce2dca06734b35b70d380ca3f70271c7f8852c922a7c62c86c4e35f0c39290565eca7133c625908d40e126ac57c02b1b1a4636b9457d77e1e60b981 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -129,6 +213,25 @@ __metadata: languageName: node linkType: hard +"protobufjs@npm:^7.5.4": + version: 7.6.5 + resolution: "protobufjs@npm:7.6.5" + dependencies: + "@protobufjs/aspromise": "npm:^1.1.2" + "@protobufjs/base64": "npm:^1.1.2" + "@protobufjs/codegen": "npm:^2.0.5" + "@protobufjs/eventemitter": "npm:^1.1.1" + "@protobufjs/fetch": "npm:^1.1.1" + "@protobufjs/float": "npm:^1.0.2" + "@protobufjs/path": "npm:^1.1.2" + "@protobufjs/pool": "npm:^1.1.0" + "@protobufjs/utf8": "npm:^1.1.1" + "@types/node": "npm:>=13.7.0" + long: "npm:^5.3.2" + checksum: 10c0/863eaca9c6f45bfcfb8787c545f53e9c6696507e328e3981b4643c12d35df7f2826021c10e3fd30bef342c13a8e9d306547bac9c0849ee3bf50f770a12f01dc5 + languageName: node + linkType: hard + "typescript@npm:5.4.5": version: 5.4.5 resolution: "typescript@npm:5.4.5" @@ -175,3 +278,10 @@ __metadata: checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d languageName: node linkType: hard + +"undici-types@npm:~8.3.0": + version: 8.3.0 + resolution: "undici-types@npm:8.3.0" + checksum: 10c0/c8aa7e2fbebfce519654dafadc0ece59be888d2ccaf180fb4495da875e7b536d2456345c384069c7e6f3e9c9ab7435f074957da306f142343eee86ff8048855a + languageName: node + linkType: hard